For making forms on the frontend I prefer using react-hook-forms + resolvers for end to end form logic along with type safety

Home

https://github.com/react-hook-form/resolvers

Resolvers helps us with type safety against a zod defined schema inside our forms

Examples

Simple Form

import { useForm } from "react-hook-form";
import z from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';

const formSchema = z.object({
	city: z.string().optional()
})

export default function App() {
  const { register, handleSubmit } = useForm({
		resolver: zodResolver(formSchema),
});
  const onSubmit = async data => { console.log(data); }; //get input data

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input
				name="city"
        id="city"
				label="city"
        placeholder="city"
        {...register("city")} // custom message
      />
      <button type="submit">Submit</button>
    </form>
  );
}

Mapping in Forms

When you are mapping an array of objects inside a form component

import { useForm, useWatch } from "react-hook-form";
import z from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';

const formSchema = z.object({
	directorName: z.string().optional()
	email: z.string().optional()
	role: z.string().optional()
})

const directorData = [
  {
    id: 1,
    directorName: "Aman",
    email: "[email protected]",
    role: "engineer"
  },
  {
    id: 2,
    directorName: "Anish",
    email: "[email protected]",
    role: "designer"
  }
];

const ExampleComponent = () => {

const {
    register,
    getFieldState,
    formState: { errors }
  } = useForm({
		resolver: zodResolver(formSchema),
    defaultValues: {
      directorArray: directorData
    }
  });

  // You can choose any name here
  const formData = useWatch({
    name: "directorArray"
  });

return (
    <form>
      <div className="h-[100%] p-4 flex flex-col gap-4 items-center justify-center h-48">
        {directorData?.map((dirInfo, index) => {
          return (
            <div
              key={dirInfo?.id}
              className="w-full flex flex-col gap-2 border-2 p-4"
            >
              <div className="w-full flex justify-end">
                <button
                  // type="submit"
									// isDirty here represents whether the input-
									// field is updating or not
                  disabled={
                    getFieldState(`directorArray.${index}`)?.isDirty
                      ? false
                      : true
                  }
                  className={
                    getFieldState(`directorArray.${index}`)?.isDirty
                      ? "bg-blue-500 p-2 rounded-md text-white"
                      : "bg-gray-300 p-2 rounded-md text-gray-400"
                  }
                >
                  Save Details
                </button>
              </div>
              <label className="flex flex-col" htmlFor="directorName">
                Director Name
                <input
                  className="border border-black p-2 rounded-md"
                  type="text"
                  name="directorName"
                  id="directorName"
                  placeholder={dirInfo?.directorName}
                  {...register(`directorArray.${index}.directorName`, {
                    required: true
                  })}
                />
              </label>
              <label className="flex flex-col" htmlFor="email">
                Director Email
                <input
                  className="border border-black p-2 rounded-md"
                  type="text"
                  name="email"
                  id="email"
                  placeholder={dirInfo?.email}
                  {...register(`directorArray.${index}.email`)}
                />
              </label>
              <label className="flex flex-col" htmlFor="role">
                Director Role
                <input
                  className="border border-black p-2 rounded-md"
                  type="text"
                  name="role"
                  id="role"
                  placeholder={dirInfo?.role}
                  {...register(`directorArray.${index}.role`)}
                />
              </label>
            </div>
          );
        })}
      </div>
    </form>
  );

}

export default ExampleComponent

This is a similar example where you are getting an address object from the backend read endpoint and integrating those fields with the form inputs

We want those fields to be editable when the user starts typing anything inside the input field,

this can be done both with normal state and react hook forms but let me show you how messy it can(not saying it is) get if you try to do it the normal react state

import React, {useState} from 'react'

const data = {
addressLine1: 'lin1',
addressLine2: 'line2',
addressLine3: 'line3',
postalCode: '12345678',
city: 'Bangalore',
}

const ExampleComponent = () => {
// state variable to track whether the user has typed anything inside the input
const [stateChange, setStateChange] = useState(false);
// state variable 
const [companyInfoState, setCompanyInfoState] = useState({
    addressLine1: data?.addressLine1,
    addressLine2: data?.addressLine2,
    addressLine3: data?.addressLine3 ?? '',
    postalCode: data?.postalCode,
    city: data?.city,
  });

// to track the state change in react
useEffect(() => {
    setStateChange(true);
  }, [state]);

const handleCompanyChange = (e) => {
    setCompanyInfoState((state) => ({
      ...state,
      [e?.target?.name]: e.target.value,
    }));
  };

const handleCompanyDetails = async () => {
    try {
      if (companyInfoState.postalCode === '') {
        return false;
      }

      if (companyInfoState.city === '') {
        return false;
      }
        if (response.ok) {
          const resData = await fetch()
          await mutate();
       
        } else {
          return false
        }
      }
    } catch (error) {
      console.log(error);
    }
  };

     

return (
    <div className="flex flex-col gap-4 bg-white rounded p-5">
      <div className="w-full mb-4 flex justify-between">
        <h4 className="text-2xl font-semibold ">Company Information</h4>
        <button
          type="submit"
          onClick={handleCompanyDetails}
          disabled={stateChange ? false : true}
          className={
            stateChange
              ? 'inline-flex items-center rounded-md border border-transparent bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-0 focus:ring-offset-0'
              : 'cursor-pointer inline-flex items-center rounded-md border border-transparent bg-gray-400 px-4 py-2 text-sm font-medium text-white focus:outline-none focus:ring-0 focus:ring-offset-0'
          }
        >
          Save Changes
        </button>
      </div>

      <div className="w-1/2 flex flex-col gap-4">
        {companyInfoData?.us_incorporation_address !== null && (
          <div className="flex flex-col space-y-1">
            <input
              name="addressLine1"
              id="addressLine1"
              label="US Incorporation Address"
              value={companyInfoState.addressLine1}
              placeholder="Address Line 1"
              onChange={handleCompanyChange}
            />

            <input
              name="addressLine2"
              id="addressLine2"
              value={companyInfoState.addressLine2}
              placeholder="Address Line 2"
              onChange={handleCompanyChange}
            />

            <input
              name="addressLine3"
              id="addressLine3"
              value={companyInfoState.addressLine3}
              placeholder="Address Line 3"
              onChange={handleCompanyChange}
            />

            <div className="w-full flex items-center gap-2">
              <input
                name="postalCode"
                id="postalCode"
                value={companyInfoState.postalCode}
                placeholder="Postal Code"
                onChange={handleCompanyChange}
              />
              <input
                name="city"
                id="city"
                value={companyInfoState.city}
                placeholder="City"
                onChange={handleCompanyChange}
              />
            </div>
          </div>
        )}
      </div>
    </div>
  );

}

This is an example of the above use case done with normal react state, the reason why RHF is much better in such cases: