forms-5-custom-hooks
Custom Hooks for Forms: Explore creating custom hooks like useForm for managing form logic, including state, validation, and submissions, and how TypeScript ensures the correctness of custom hook logic and returned values.
When preparing for an intermediate/advanced React TypeScript interview, discussing custom hooks for form management is essential. Here are ten topics related to custom hooks for forms that you should be familiar with, along with code examples to illustrate them:
1. Creating a Basic useForm
Hook
Start by explaining how to create a useForm
hook for basic form handling and how TypeScript ensures the correct types for form values.
import { useState } from 'react';
interface FormValues {
[key: string]: any;
}
export const useForm = <T extends FormValues>(initialValues: T) => {
const [values, setValues] = useState<T>(initialValues);
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = event.target;
setValues({
...values,
[name]: value,
});
};
return {
values,
handleChange,
};
};
Example usage
import React from 'react';
import { useForm } from './path-to-useForm-hook';
// Define the initial form state type
interface MyFormState {
username: string;
email: string;
// add other form fields here
}
const MyFormComponent = () => {
// Initialize useForm with the initial form state
const { values, handleChange } = useForm<MyFormState>({
username: '',
email: '',
// set initial values for other form fields
});
const handleSubmit = (event: React.FormEvent) => {
event.preventDefault();
// Handle form submission using the 'values' state
console.log(values);
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
name="username"
value={values.username}
onChange={handleChange}
/>
<input
type="email"
name="email"
value={values.email}
onChange={handleChange}
/>
{/* Add other form inputs here */}
<button type="submit">Submit</button>
</form>
);
};
export default MyFormComponent;
2. Adding Form Submission to useForm
Discuss how to extend the useForm
hook to handle form submissions, ensuring the submission function is typed correctly.
export const useForm = <T extends FormValues>(initialValues: T, onSubmit: (values: T) => void) => {
// ...previous code
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
onSubmit(values);
};
return {
values,
handleChange,
handleSubmit,
};
};
3. Incorporating Validation Logic
Show how to add validation logic to your custom hook and how TypeScript can ensure that validation rules match the form values.
interface ValidationRules<T> {
[K in keyof T]?: (value: T[K]) => string | undefined;
}
export const useForm = <T extends FormValues>(
initialValues: T,
validationRules: ValidationRules<T>
) => {
const [values, setValues] = useState<T>(initialValues);
const [errors, setErrors] = useState<FormValues>({});
// ...previous code
const validate = () => {
let valid = true;
const newErrors: FormValues = {};
for (const key in validationRules) {
const value = values[key];
const rule = validationRules[key];
const error = rule ? rule(value) : undefined;
if (error) {
valid = false;
newErrors[key] = error;
}
}
setErrors(newErrors);
return valid;
};
return {
// ...previous returns
validate,
errors,
};
};
4. Handling Typed Form Fields
Illustrate how to handle typed form fields, such as number inputs, ensuring that TypeScript maintains the correct types throughout the form state.
// Extend FormValues to handle different field types
interface FormValues {
[key: string]: string | number | boolean;
}
// Inside your hook, handle different input types accordingly
5. Dynamic Form Fields
Discuss managing dynamic form fields, such as arrays or nested objects, and how TypeScript ensures that the form values adhere to a defined structure.
interface NestedFormValues {
user: {
firstName: string;
lastName: string;
};
hobbies: string[];
}
// Inside your useForm hook, ensure that handleChange and other functions can handle nested structures
6. Debounced Validation
Explain how to implement debounced validation in your useForm
hook and how TypeScript can help with debouncing function types.
import { useEffect, useState } from 'react';
import { debounce } from 'lodash';
// ...previous useForm code
export const useForm = <T extends FormValues>(
initialValues: T,
validationRules: ValidationRules<T>
) => {
// ...previous code
const debouncedValidate = debounce(validate, 300);
useEffect(() => {
if (Object.keys(errors).length) {
debouncedValidate();
}
}, [values]);
return {
// ...previous returns
errors,
};
};
7. Custom Validation Hooks
Discuss creating separate custom hooks for validation and how TypeScript can ensure these hooks work seamlessly with useForm
.
export const useValidation = <T>(value: T, rules: Array<(value: T) => string | undefined>) => {
const [error, setError] = useState<string | undefined>();
useEffect(() => {
for (const rule of rules) {
const validationError = rule(value);
if (validationError) {
setError(validationError);
break;
}
}
}, [value]);
return error;
};
8. Type-Driven Form Components
Illustrate creating form components that are driven by TypeScript types, ensuring that form components like TextInput
or Select
only accept form values that
match their expected type.
interface TextInputProps {
value: string;
onChange: (value: string) => void;
// Other props
}
const TextInput: React.FC<TextInputProps> = ({ value, onChange, ...props }) => {
// TextInput implementation
};
9. Type-Safe Form Context
Show how to use React Context with TypeScript for form state management, providing type-safe access to form state and functions throughout the component tree.
interface FormContextValue<T> {
values: T;
handleChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
}
const FormContext = React.createContext<FormContextValue<any> | undefined>(undefined);
export const FormProvider = <T extends FormValues>({ children, ...rest }: PropsWithChildren<{ initialValues: T }>) => {
const form = useForm(rest.initialValues);
return <FormContext.Provider value={form}>{children}</FormContext.Provider>;
};
10. Form State Management with Reducers
Finally, delve into using useReducer for complex form state management scenarios and how TypeScript ensures reducers and actions are correctly typed.
interface State {
values: FormValues;
errors: FormValues;
}
type Action =
| { type: 'SET_VALUE'; payload: { name: string; value: any } }
| { type: 'SET_ERRORS'; payload: FormValues };
function formReducer(state: State, action: Action): State {
switch (action.type) {
case 'SET_VALUE':
return {
...state,
values: { ...state.values, [action.payload.name]: action.payload.value },
};
case 'SET_ERRORS':
return {
...state,
errors: action.payload,
};
default:
return state;
}
}
// useReducer hook with initial state and reducer function
By mastering these topics and their relevant TypeScript integrations, you'll showcase an advanced understanding of form handling in React with TypeScript, which will significantly impress in an interview setting.