React Hook Form - Combined Add/Edit (Create/Update) Form Example

Tutorial built with React 16.13.1 and React Hook Form 6.9.2

Other versions available:

This is a quick example of how to build a form in React with the React Hook Form library that supports both create and update modes. The form in the example is for creating and updating user data, but the same pattern could be used to build an add/edit form for any type of data.

The below components are part of a React CRUD example app I posted recently that includes a live demo, so to see the below code running check out React - CRUD Example with React Hook Form.

React Hook Form Add/Edit Component

The AddEdit component is used for both adding and editing users, it contains a form built with the React Hook Form library.

Form validation rules are defined with the Yup schema validation library and passed to the React Hook Form useForm() function, for more info on Yup see

The useForm() hook function returns an object with methods for working with a form including registering inputs, handling form submit, resetting the form, setting input values, displaying errors and more, for a complete list see

The onSubmit function gets called when the form is submitted and valid, and either creates or updates a user depending on which mode it is in.

The form is in "add mode" when there is no user id parameter (, otherwise it is in "edit mode". The variable isAddMode is used to change the form behaviour based on the mode it is in, for example in "add mode" the password field is required, and in "edit mode" (!isAddMode) the user service is called to get the user details and set the field values.

The returned JSX template contains the form with all of the input fields and validation messages. The form fields are registered with the React Hook Form using the ref={register} attribute which registers each input with the input name. For more info on form validation with React Hook Form see React - Form Validation Example with React Hook Form.

import React, { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { useForm } from "react-hook-form";
import { yupResolver } from '@hookform/resolvers/yup';
import * as Yup from 'yup';

import { userService, alertService } from '@/_services';

function AddEdit({ history, match }) {
    const { id } = match.params;
    const isAddMode = !id;
    // form validation rules 
    const validationSchema = Yup.object().shape({
        title: Yup.string()
            .required('Title is required'),
        firstName: Yup.string()
            .required('First Name is required'),
        lastName: Yup.string()
            .required('Last Name is required'),
        email: Yup.string()
            .email('Email is invalid')
            .required('Email is required'),
        role: Yup.string()
            .required('Role is required'),
        password: Yup.string()
            .transform(x => x === '' ? undefined : x)
            .concat(isAddMode ? Yup.string().required('Password is required') : null)
            .min(6, 'Password must be at least 6 characters'),
        confirmPassword: Yup.string()
            .transform(x => x === '' ? undefined : x)
            .when('password', (password, schema) => {
                if (password || isAddMode) return schema.required('Confirm Password is required');
            .oneOf([Yup.ref('password')], 'Passwords must match')

    // functions to build form returned by useForm() hook
    const { register, handleSubmit, reset, setValue, getValues, errors, formState } = useForm({
        resolver: yupResolver(validationSchema)

    function onSubmit(data) {
        return isAddMode
            ? createUser(data)
            : updateUser(id, data);

    function createUser(data) {
        return userService.create(data)
            .then(() => {
                alertService.success('User added', { keepAfterRouteChange: true });

    function updateUser(id, data) {
        return userService.update(id, data)
            .then(() => {
                alertService.success('User updated', { keepAfterRouteChange: true });

    const [user, setUser] = useState({});
    const [showPassword, setShowPassword] = useState(false);

    useEffect(() => {
        if (!isAddMode) {
            // get user and set form fields
            userService.getById(id).then(user => {
                const fields = ['title', 'firstName', 'lastName', 'email', 'role'];
                fields.forEach(field => setValue(field, user[field]));
    }, []);

    return (
        <form onSubmit={handleSubmit(onSubmit)} onReset={reset}>
            <h1>{isAddMode ? 'Add User' : 'Edit User'}</h1>
            <div className="form-row">
                <div className="form-group col">
                    <select name="title" ref={register} className={`form-control ${errors.title ? 'is-invalid' : ''}`}>
                        <option value=""></option>
                        <option value="Mr">Mr</option>
                        <option value="Mrs">Mrs</option>
                        <option value="Miss">Miss</option>
                        <option value="Ms">Ms</option>
                    <div className="invalid-feedback">{errors.title?.message}</div>
                <div className="form-group col-5">
                    <label>First Name</label>
                    <input name="firstName" type="text" ref={register} className={`form-control ${errors.firstName ? 'is-invalid' : ''}`} />
                    <div className="invalid-feedback">{errors.firstName?.message}</div>
                <div className="form-group col-5">
                    <label>Last Name</label>
                    <input name="lastName" type="text" ref={register} className={`form-control ${errors.lastName ? 'is-invalid' : ''}`} />
                    <div className="invalid-feedback">{errors.lastName?.message}</div>
            <div className="form-row">
                <div className="form-group col-7">
                    <input name="email" type="text" ref={register} className={`form-control ${ ? 'is-invalid' : ''}`} />
                    <div className="invalid-feedback">{}</div>
                <div className="form-group col">
                    <select name="role" ref={register} className={`form-control ${errors.role ? 'is-invalid' : ''}`}>
                        <option value=""></option>
                        <option value="User">User</option>
                        <option value="Admin">Admin</option>
                    <div className="invalid-feedback">{errors.role?.message}</div>
            {!isAddMode &&
                    <h3 className="pt-3">Change Password</h3>
                    <p>Leave blank to keep the same password</p>
            <div className="form-row">
                <div className="form-group col">
                        {!isAddMode &&
                                ? <span> - <a onClick={() => setShowPassword(!showPassword)} className="text-primary">Show</a></span>
                                : <em> - {user.password}</em>
                    <input name="password" type="password" ref={register} className={`form-control ${errors.password ? 'is-invalid' : ''}`} />
                    <div className="invalid-feedback">{errors.password?.message}</div>
                <div className="form-group col">
                    <label>Confirm Password</label>
                    <input name="confirmPassword" type="password" ref={register} className={`form-control ${errors.confirmPassword ? 'is-invalid' : ''}`} />
                    <div className="invalid-feedback">{errors.confirmPassword?.message}</div>
            <div className="form-group">
                <button type="submit" disabled={formState.isSubmitting} className="btn btn-primary">
                    {formState.isSubmitting && <span className="spinner-border spinner-border-sm mr-1"></span>}
                <Link to={isAddMode ? '.' : '..'} className="btn btn-link">Cancel</Link>

export { AddEdit };

User Routes Component

The Users component defines routes for each of the pages within the users section.

The second and third routes are for adding and editing users, they match different routes but both load the above AddEdit component which modifies its behaviour based on the route.

import React from 'react';
import { Route, Switch } from 'react-router-dom';

import { List } from './List';
import { AddEdit } from './AddEdit';

function Users({ match }) {
    const { path } = match;
    return (
            <Route exact path={path} component={List} />
            <Route path={`${path}/add`} component={AddEdit} />
            <Route path={`${path}/edit/:id`} component={AddEdit} />

export { Users };


Subscribe or Follow Me For Updates

Subscribe to my YouTube channel or follow me on Twitter or GitHub to be notified when I post new content.


Supported by