DEV Community

Hasan
Hasan

Posted on

Mastering Form Handling with Custom Hooks in React

Handling forms in React can be challenging, especially as forms grow in complexity. Custom hooks provide an elegant solution to manage form state, validation, and submission logic, making your code cleaner and more maintainable. In this blog post, we'll explore how to create custom hooks for form handling in React using TypeScript.

Table of Contents

  1. Introduction to Form Handling in React
  2. Creating a Basic useForm Hook with TypeScript
  3. Adding Validation Logic
  4. Managing Form Submission
  5. Handling Form Reset
  6. Integrating with External Libraries
  7. Advanced Form Handling Techniques
  8. Best Practices for Form Handling Hooks
  9. Example: Building a Complete Form
  10. Conclusion

1. Introduction to Form Handling in React

Forms are essential in web applications for user interaction, but managing form state and validation can quickly become cumbersome. React's controlled components approach, where form elements derive their values from state, helps keep form state predictable. Custom hooks allow us to abstract and reuse form logic efficiently.

2. Creating a Basic useForm Hook

Let's start by creating a simple useForm hook to manage form state.

import { useState } from 'react';

type FormValues = {
    [key: string]: any;
};

function 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,
    };
}

export default useForm;
Enter fullscreen mode Exit fullscreen mode

This hook initializes form values and provides a handleChange function to update the state.

3. Adding Validation Logic

Next, let's add validation logic to our useForm hook. We'll accept a validate function as a parameter and manage validation errors.

import { useState } from 'react';

type FormValues = {
    [key: string]: any;
};

type Errors<T> = {
    [K in keyof T]?: string;
};

function useForm<T extends FormValues>(
    initialValues: T,
    validate: (name: keyof T, value: T[keyof T]) => string | undefined
) {
    const [values, setValues] = useState<T>(initialValues);
    const [errors, setErrors] = useState<Errors<T>>({});

    const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
        const { name, value } = event.target;
        setValues({
            ...values,
            [name]: value,
        });

        if (validate) {
            setErrors({
                ...errors,
                [name]: validate(name as keyof T, value),
            });
        }
    };

    return {
        values,
        errors,
        handleChange,
    };
}

export default useForm;
Enter fullscreen mode Exit fullscreen mode

This hook now tracks errors and updates them based on the validation logic provided.

4. Managing Form Submission

Now, we'll add a handleSubmit function to handle form submission.

import { useState } from 'react';

type FormValues = {
    [key: string]: any;
};

type Errors<T> = {
    [K in keyof T]?: string;
};

function useForm<T extends FormValues>(
    initialValues: T,
    validate: (name: keyof T, value: T[keyof T]) => string | undefined,
    onSubmit: (values: T) => void
) {
    const [values, setValues] = useState<T>(initialValues);
    const [errors, setErrors] = useState<Errors<T>>({});

    const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
        const { name, value } = event.target;
        setValues({
            ...values,
            [name]: value,
        });

        if (validate) {
            setErrors({
                ...errors,
                [name]: validate(name as keyof T, value),
            });
        }
    };

    const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
        event.preventDefault();
        const validationErrors: Errors<T> = {};
        for (const key in values) {
            const error = validate(key as keyof T, values[key]);
            if (error) {
                validationErrors[key as keyof T] = error;
            }
        }
        setErrors(validationErrors);

        if (Object.keys(validationErrors).length === 0) {
            onSubmit(values);
        }
    };

    return {
        values,
        errors,
        handleChange,
        handleSubmit,
    };
}

export default useForm;
Enter fullscreen mode Exit fullscreen mode

The handleSubmit function prevents the default form submission, validates all fields, and calls the onSubmit function if there are no errors.

5. Handling Form Reset

We can also add a resetForm function to reset the form to its initial state.

import { useState } from 'react';

type FormValues = {
    [key: string]: any;
};

type Errors<T> = {
    [K in keyof T]?: string;
};

function useForm<T extends FormValues>(
    initialValues: T,
    validate: (name: keyof T, value: T[keyof T]) => string | undefined,
    onSubmit: (values: T) => void
) {
    const [values, setValues] = useState<T>(initialValues);
    const [errors, setErrors] = useState<Errors<T>>({});

    const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
        const { name, value } = event.target;
        setValues({
            ...values,
            [name]: value,
        });

        if (validate) {
            setErrors({
                ...errors,
                [name]: validate(name as keyof T, value),
            });
        }
    };

    const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
        event.preventDefault();
        const validationErrors: Errors<T> = {};
        for (const key in values) {
            const error = validate(key as keyof T, values[key]);
            if (error) {
                validationErrors[key as keyof T] = error;
            }
        }
        setErrors(validationErrors);

        if (Object.keys(validationErrors).length === 0) {
            onSubmit(values);
        }
    };

    const resetForm = () => {
        setValues(initialValues);
        setErrors({});
    };

    return {
        values,
        errors,
        handleChange,
        handleSubmit,
        resetForm,
    };
}

export default useForm;
Enter fullscreen mode Exit fullscreen mode

6. Integrating with External Libraries

Our custom hook can be easily integrated with external libraries like Yup for validation.

import * as Yup from 'yup';

const validationSchema = Yup.object().shape({
    username: Yup.string().required('Username is required'),
    email: Yup.string().email('Invalid email address').required('Email is required'),
});

function validate(name: string, value: any) {
    try {
        validationSchema.validateSyncAt(name, { [name]: value });
        return '';
    } catch (error) {
        return error.message;
    }
}
Enter fullscreen mode Exit fullscreen mode

7. Advanced Form Handling Techniques

Custom hooks can also handle more complex scenarios, such as dynamic forms, dependent fields, and multi-step forms. These advanced techniques involve managing more intricate state and logic within your hooks.

8. Best Practices for Form Handling Hooks

Keep It Simple: Start with basic functionality and extend as needed.
Separate Concerns: Handle validation, submission, and state management in distinct functions if they become too complex.
Reusability: Make sure your custom hooks are reusable across different forms.
Type Safety: Utilize TypeScript to ensure your custom hooks and form components are type-safe.
Testing: Write tests for your custom hooks to ensure they work as expected.

9. Example: Building a Complete Form

Here's how to use our custom useForm hook to build a complete form.

import React from 'react';
import useForm from './useForm';

const validate = (name: string, value: any) => {
    if (!value) return `${name} is required`;
    return '';
};

const onSubmit = (values: { username: string; email: string }) => {
    console.log('Form Submitted:', values);
};

const App: React.FC = () => {
    const { values, errors, handleChange, handleSubmit, resetForm } = useForm(
        { username: '', email: '' },
        validate,
        onSubmit
    );

    return (
        <form onSubmit={handleSubmit}>
            <div>
                <label>
                    Username:
                    <input type="text" name="username" value={values.username} onChange={handleChange} />
                    {errors.username && <span>{errors.username}</span>}
                </label>
            </div>
            <div>
                <label>
                    Email:
                    <input type="email" name="email" value={values.email} onChange={handleChange} />
                    {errors.email && <span>{errors.email}</span>}
                </label>
            </div>
            <button type="submit">Submit</button>
            <button type="button" onClick={resetForm}>Reset</button>
        </form>
    );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

10. Conclusion

Custom hooks in React provide a powerful way to manage form state, validation, and submission logic. By encapsulating this logic within hooks, you can create reusable and maintainable form components. Start with the basics, and gradually add more functionality as needed.

Top comments (0)