Nextjs
React Hook Form
ReferenceEfficient, adaptable, and scalable forms with straightforward validation.
Example Usages
Demonstration of usage with Material-UI components and form validation
Is Valid: false
Form data
{
"Native": "",
"TextField": "",
"Select": "",
"Autocomplete": [],
"Checkbox": false,
"Switch": false,
"RadioGroup": "male",
"DateTimePicker": ""
}Touched fields
{}Render Count: 17
import { Controller, useForm } from 'react-hook-form';
import Button from '@mui/material/Button';
import TextField from '@mui/material/TextField';
import Checkbox from '@mui/material/Checkbox';
import Select from '@mui/material/Select';
import MenuItem from '@mui/material/MenuItem';
import Switch from '@mui/material/Switch';
import RadioGroup from '@mui/material/RadioGroup';
import FormControlLabel from '@mui/material/FormControlLabel';
import Radio from '@mui/material/Radio';
import Typography from '@mui/material/Typography';
import Autocomplete from '@mui/material/Autocomplete';
import _ from 'lodash';
import clsx from 'clsx';
import FormControl from '@mui/material/FormControl';
import FormLabel from '@mui/material/FormLabel';
import FormHelperText from '@mui/material/FormHelperText';
import { DateTimePicker } from '@mui/x-date-pickers';
import SingularitySvgIcon from '@singularity/core/SingularitySvgIcon';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { DevTool } from '@hookform/devtools';
let renderCount = 0;
const options = [
{
value: 'chocolate',
label: 'Chocolate'
},
{
value: 'strawberry',
label: 'Strawberry'
},
{
value: 'vanilla',
label: 'Vanilla'
}
];
/**
* Form Validation Schema
*/
const schema = z.object({
TextField: z.string().nonempty('You must enter a value'),
Native: z.string().nonempty('You must enter a value'),
Select: z
.string()
.nonempty('You must select a value')
.refine((val) => ['20', '30'].includes(val), 'Select 20 or 30.'),
Checkbox: z.boolean().refine((val) => val === true, 'You must check.'),
Switch: z.boolean().refine((val) => val === true, 'You must turn it on.'),
RadioGroup: z.string().superRefine((val, ctx) => {
if (val !== 'female') {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'You must select female.'
});
}
}),
Autocomplete: z.array(z.string()).min(2, 'Select at least two.'),
DateTimePicker: z.string().refine((val) => val === null || val.trim().length > 0, 'You must select a date')
});
type FormType = z.infer<typeof schema>;
const defaultValues: FormType = {
Native: '',
TextField: '',
Select: '',
Autocomplete: [],
Checkbox: false,
Switch: false,
RadioGroup: 'male',
DateTimePicker: ''
};
/**
* Simple Form Example
*/
function SimpleFormExample() {
const { handleSubmit, register, reset, control, watch, formState } = useForm<FormType>({
defaultValues,
mode: 'all',
resolver: zodResolver(schema)
});
const { isValid, dirtyFields, errors, touchedFields } = formState;
renderCount += 1;
const data = watch();
return (
<div className="flex w-full max-w-screen-md justify-start items-start">
<form
className="w-1/2"
// eslint-disable-next-line no-console
onSubmit={handleSubmit((_data) => console.info(_data))}
>
<div className="mt-12 mb-4">
<Typography className="mb-6 font-medium text-base">Native Input:</Typography>
<input
className={clsx('border-1 outline-hidden rounded-lg p-2', !!errors.Native && 'border-red')}
{...register('Native')}
required
/>
{!!errors.Native && (
<Typography
className="px-1 py-2 font-medium text-base"
color="error"
>
{errors?.Native?.message}
</Typography>
)}
</div>
<div className="mt-12 mb-4">
<Controller
name="Checkbox"
control={control}
render={({ field: { onChange, value, onBlur, ref } }) => (
<FormControl
error={!!errors.Checkbox}
required
>
<FormLabel
className="font-medium text-base"
component="legend"
>
MUI Checkbox
</FormLabel>
<FormControlLabel
label="I agree"
control={
<Checkbox
checked={value}
onBlur={onBlur}
onChange={(ev) => onChange(ev.target.checked)}
inputRef={ref}
required
/>
}
/>
<FormHelperText>{errors?.Checkbox?.message}</FormHelperText>
</FormControl>
)}
/>
</div>
<div className="mt-12 mb-4">
<Controller
render={({ field }) => (
<FormControl
error={!!errors.RadioGroup}
required
>
<FormLabel
className="font-medium text-base"
component="legend"
>
Radio Group
</FormLabel>
<RadioGroup
{...field}
aria-label="gender"
name="gender1"
>
<FormControlLabel
value="female"
control={<Radio />}
label="Female"
/>
<FormControlLabel
value="male"
control={<Radio />}
label="Male"
/>
</RadioGroup>
<FormHelperText>{errors?.RadioGroup?.message}</FormHelperText>
</FormControl>
)}
name="RadioGroup"
control={control}
/>
</div>
<div className="mt-12 mb-4">
<Controller
render={({ field }) => (
<TextField
{...field}
label="MUI TextField"
variant="outlined"
error={!!errors.TextField}
helperText={errors?.TextField?.message}
required
fullWidth
/>
)}
name="TextField"
control={control}
/>
</div>
<div className="mt-12 mb-4">
<Controller
render={({ field }) => (
<FormControl
error={!!errors.Select}
required
fullWidth
>
<FormLabel
className="font-medium text-base"
component="legend"
>
MUI Select
</FormLabel>
<Select
{...field}
variant="outlined"
fullWidth
>
<MenuItem value="10">Ten (10)</MenuItem>
<MenuItem value="20">Twenty (20)</MenuItem>
<MenuItem value="30">Thirty (30)</MenuItem>
</Select>
<FormHelperText>{errors?.Select?.message}</FormHelperText>
</FormControl>
)}
name="Select"
control={control}
/>
</div>
<div className="mt-12 mb-4">
<Controller
name="Switch"
control={control}
render={({ field: { onChange, value, ref, onBlur } }) => (
<FormControl
required
error={!!errors.Switch}
>
<FormLabel
className="font-medium text-base"
component="legend"
>
MUI Switch
</FormLabel>
<Switch
checked={value}
onBlur={onBlur}
onChange={(ev) => onChange(ev.target.checked)}
inputRef={ref}
required
/>
<FormHelperText>{errors?.Switch?.message}</FormHelperText>
</FormControl>
)}
/>
</div>
<div className="mt-12 mb-4">
<Typography className="mb-6 font-medium text-base">Autocomplete</Typography>
<Controller
name="Autocomplete"
control={control}
render={({ field: { onChange, value, onBlur, ref } }) => (
<Autocomplete
className="mt-2 mb-4"
multiple
options={options.map((option) => option.value)}
getOptionLabel={(option) => options.find((o) => o.value === option)?.label || ''}
renderInput={(params) => (
<TextField
{...params}
placeholder="Select multiple tags"
label="Tags"
variant="outlined"
slotProps={{
inputLabel: {
shrink: true
}
}}
error={!!errors.Autocomplete}
helperText={errors?.Autocomplete?.message}
onBlur={onBlur}
inputRef={ref}
/>
)}
filterSelectedOptions
value={value || []}
onChange={(_event, newValue) => {
onChange(newValue);
}}
/>
)}
/>
</div>
<div className="mt-12 mb-4">
<Typography className="mb-6 font-medium text-base">DateTimePicker</Typography>
<Controller
name="DateTimePicker"
control={control}
render={({ field: { onChange, value } }) => (
<DateTimePicker
value={new Date(value)}
onChange={(val) => {
onChange(val.toISOString());
}}
slotProps={{
textField: {
id: 'birthday',
label: 'Birthday',
InputLabelProps: {
shrink: true
},
fullWidth: true,
variant: 'outlined',
error: !!errors.DateTimePicker,
helperText: errors?.DateTimePicker?.message
},
inputAdornment: {
position: 'start',
children: <SingularitySvgIcon size={20}>heroicons-solid:cake</SingularitySvgIcon>
}
}}
/>
)}
/>
</div>
<div className="flex my-12 items-center">
<Button
className="mx-2"
variant="contained"
color="secondary"
type="submit"
disabled={_.isEmpty(dirtyFields) || !isValid}
>
Submit
</Button>
<Button
className="mx-2"
type="button"
onClick={() => {
reset(defaultValues);
}}
>
Reset Form
</Button>
</div>
</form>
<div className="w-1/2 my-12 p-6">
<div className="mb-3">
<Typography>Is Valid: {isValid ? 'true' : 'false'}</Typography>
</div>
<div className="mb-3">
<Typography>Form data</Typography>
</div>
<div className="mb-3">
<pre className="language-js p-6 w-100">{JSON.stringify(data, null, 2)}</pre>
</div>
<div className="mb-3">
<Typography>Touched fields</Typography>
<pre className="language-js p-6 w-100">{JSON.stringify(touchedFields, null, 2)}</pre>
</div>
<div className="mb-3">
<Typography
className="mt-4 font-medium text-md italic"
color="text.secondary"
>
Render Count: {renderCount}
</Typography>
</div>
</div>
<DevTool
control={control}
styles={{ button: { position: 'relative' } }}
/>
</div>
);
}
export default SimpleFormExample;Demos
- @/app/(control-panel)/sign-in/SignInPage.tsx
- @/app/(control-panel)/sign-up/SignUpPage.tsx
- .
- .
- .