Skip to main content

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.