Published: August 31 2021

Next.js - Combined Add/Edit (Create/Update) Form Example

Tutorial built with Next.js 11.1.0 and React Hook Form 7.12.2

Other versions available:

This is a quick example of how to build a form in Next.js 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 Next.js login tutorial app I posted recently that includes a live demo, so to see the below code running check out Next.js 11 - User Registration and Login Tutorial with Example App.


Next.js Users Add/Edit Component

Path: /components/users/AddEdit.jsx

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

Form validation rules are defined with the Yup schema validation library and passed with the formOptions 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, accessing form state, 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 object included in the component props (props.user), 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 details are assigned to the form default values to pre-populate the form when it loads.

The returned JSX template contains the form markup with all input fields and validation messages. The form fields are registered with the React Hook Form by calling the register() function with the field name from each input element (e.g. {...register('firstName')}). For more info on form validation with React Hook Form see React Hook Form 7 - Form Validation Example.

import { useRouter } from 'next/router';
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import * as Yup from 'yup';

import { Link } from 'components';
import { userService, alertService } from 'services';

export { AddEdit };

function AddEdit(props) {
    const user = props?.user;
    const isAddMode = !user;
    const router = useRouter();
    // form validation rules 
    const validationSchema = Yup.object().shape({
        firstName: Yup.string()
            .required('First Name is required'),
        lastName: Yup.string()
            .required('Last Name is required'),
        username: Yup.string()
            .required('Username 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')
    const formOptions = { resolver: yupResolver(validationSchema) };

    // set default form values if in edit mode
    if (!isAddMode) {
        formOptions.defaultValues = props.user;

    // get functions to build form with useForm() hook
    const { register, handleSubmit, reset, formState } = useForm(formOptions);
    const { errors } = formState;

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

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

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

    return (
        <form onSubmit={handleSubmit(onSubmit)}>
            <div className="form-row">
                <div className="form-group col">
                    <label>First Name</label>
                    <input name="firstName" type="text" {...register('firstName')} className={`form-control ${errors.firstName ? 'is-invalid' : ''}`} />
                    <div className="invalid-feedback">{errors.firstName?.message}</div>
                <div className="form-group col">
                    <label>Last Name</label>
                    <input name="lastName" type="text" {...register('lastName')} className={`form-control ${errors.lastName ? 'is-invalid' : ''}`} />
                    <div className="invalid-feedback">{errors.lastName?.message}</div>
            <div className="form-row">
                <div className="form-group col">
                    <input name="username" type="text" {...register('username')} className={`form-control ${errors.username ? 'is-invalid' : ''}`} />
                    <div className="invalid-feedback">{}</div>
                <div className="form-group col">
                        {!isAddMode && <em className="ml-1">(Leave blank to keep the same password)</em>}
                    <input name="password" type="password" {...register('password')} className={`form-control ${errors.password ? 'is-invalid' : ''}`} />
                    <div className="invalid-feedback">{errors.password?.message}</div>
            <div className="form-group">
                <button type="submit" disabled={formState.isSubmitting} className="btn btn-primary mr-2">
                    {formState.isSubmitting && <span className="spinner-border spinner-border-sm mr-1"></span>}
                <button onClick={() => reset(formOptions.defaultValues)} type="button" disabled={formState.isSubmitting} className="btn btn-secondary">Reset</button>
                <Link href="/users" className="btn btn-link">Cancel</Link>

Next.js Add User Page

Path: /pages/users/add.jsx

The add user page is a thin wrapper around the add/edit user component without any user specified which sets the add/edit component to be in "add" mode.

import { Layout, AddEdit } from 'components/users';

export default Add;

function Add() {
    return (
            <h1>Add User</h1>
            <AddEdit />

Next.js Edit User Page

Path: /pages/users/edit/[id].jsx

The edit user page wraps the add/edit user component and sets the component to "edit" mode by passing it the user object specified by the id parameter. The user is fetched from the API in a useEffect() React hook which runs when the component is mounted, the id is passed to the component by the getServerSideProps() function on page load. A loading spinner component is displayed while the user is being fetched.

import { useState, useEffect } from 'react';

import { Layout, AddEdit } from 'components/users';
import { Spinner } from 'components';
import { userService, alertService } from 'services';

export default Edit;

function Edit({ id }) {
    const [user, setUser] = useState(null);

    useEffect(() => {
        // fetch user and set default form values if in edit mode
            .then(x => setUser(x))

        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, []);

    return (
            <h1>Edit User</h1>
            {user ? <AddEdit user={user} /> : <Spinner /> }

export async function getServerSideProps({ params }) {
    return {
        props: { id: }


Need Some NextJS Help?

Search fiverr for freelance NextJS developers.

Follow me for updates

On Twitter or RSS.

When I'm not coding...

Me and Tina are on a motorcycle adventure around Australia.
Come along for the ride!


Supported by