For making forms on the frontend I prefer using react-hook-forms + resolvers for end to end form logic along with type safety
https://github.com/react-hook-form/resolvers
Resolvers helps us with type safety against a zod defined schema inside our forms
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>
);
}
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: