Skip to content

React Hook Form

How to use React Hook Form with the React components of the NayaOne Design System.

register() works for components that forward a ref to a native input. Select, SelectInput, and other select-like components keep their own value, so they need a controlled value / onValueChange pair.

Terminal window
bun install react-hook-form
Terminal window
npm install react-hook-form

TextInput, TextAreaInput, Password, PasswordInput, and the native controls on Field (Field.Input, Field.Textarea, Field.Select) forward ref to a real element. Spread the object returned by register() onto the component, and pass invalid and error from formState to show a validation message.

Set noValidate on the form so React Hook Form runs the rules.

import { useForm } from 'react-hook-form';
import {
TextAreaInput,
TextInput,
} from '@fintech-sandpit/ui/react';
type ProfileFormValues = {
name: string;
bio: string;
};
const ProfileForm = () => {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<ProfileFormValues>();
const onSubmit = (values: ProfileFormValues) => {
// send values
};
return (
<form
noValidate
onSubmit={handleSubmit(onSubmit)}
>
<TextInput
{...register('name', { required: 'Name is required' })}
label="Name"
placeholder="Jane Doe"
invalid={Boolean(errors.name)}
error={errors.name?.message}
required
/>
<TextAreaInput
{...register('bio')}
label="Bio"
placeholder="A short bio"
rows={3}
invalid={Boolean(errors.bio)}
error={errors.bio?.message}
/>
</form>
);
};

Select and SelectInput read and write their value through value and onValueChange. Connect that pair with Controller.

Give the field a defaultValues entry so the select stays controlled. With multiple, both value and the onValueChange argument are string[].

The same value / onValueChange pair applies to Combobox, ListBox, NumberInput, DatePicker, ColorInput, TagInput, Switch, Radio, and RadioCard. Checkbox is controlled with checked and onCheckedChange.

import {
Controller,
useForm,
} from 'react-hook-form';
import {
createListCollection,
SelectInput,
type ListCollectionItem,
} from '@fintech-sandpit/ui/react';
type PetFormValues = {
pet: string;
};
const pets = createListCollection<ListCollectionItem>({
items: [
{ label: 'Dog', value: 'dog' },
{ label: 'Cat', value: 'cat' },
{ label: 'Parrot', value: 'parrot' },
],
});
const PetForm = () => {
const {
control,
handleSubmit,
formState: { errors },
} = useForm<PetFormValues>({
defaultValues: {
pet: '',
},
});
const onSubmit = (values: PetFormValues) => {
// send values
};
return (
<form
noValidate
onSubmit={handleSubmit(onSubmit)}
>
<Controller
name="pet"
control={control}
rules={{ required: 'Please select a pet' }}
render={({ field }) => (
<SelectInput
name={field.name}
collection={pets}
label="Pet"
placeholder="Select a pet"
value={field.value}
invalid={Boolean(errors.pet)}
error={errors.pet?.message}
required
onBlur={field.onBlur}
onValueChange={value => field.onChange(value ?? '')}
/>
)}
/>
</form>
);
};

Call useForm once in the parent and pass the return value to FormProvider. Child components read the same form with useFormContext. Native inputs still use register(). Select-like inputs still use Controller with value and onValueChange.

import {
Controller,
FormProvider,
useForm,
useFormContext,
} from 'react-hook-form';
import {
createListCollection,
SelectInput,
TextInput,
type ListCollectionItem,
} from '@fintech-sandpit/ui/react';
type SignupFormValues = {
name: string;
role: string;
};
const roles = createListCollection<ListCollectionItem>({
items: [
{ label: 'Admin', value: 'admin' },
{ label: 'Member', value: 'member' },
],
});
const NameField = () => {
const {
register,
formState: { errors },
} = useFormContext<SignupFormValues>();
return (
<TextInput
{...register('name', { required: 'Name is required' })}
label="Name"
placeholder="Jane Doe"
invalid={Boolean(errors.name)}
error={errors.name?.message}
required
/>
);
};
const RoleField = () => {
const {
control,
formState: { errors },
} = useFormContext<SignupFormValues>();
return (
<Controller
name="role"
control={control}
rules={{ required: 'Please select a role' }}
render={({ field }) => (
<SelectInput
name={field.name}
collection={roles}
label="Role"
placeholder="Select a role"
value={field.value}
invalid={Boolean(errors.role)}
error={errors.role?.message}
required
onBlur={field.onBlur}
onValueChange={value => field.onChange(value ?? '')}
/>
)}
/>
);
};
const SignupForm = () => {
const form = useForm<SignupFormValues>({
defaultValues: {
name: '',
role: '',
},
});
const onSubmit = (values: SignupFormValues) => {
// send values
};
return (
<FormProvider {...form}>
<form
noValidate
onSubmit={form.handleSubmit(onSubmit)}
>
<NameField />
<RoleField />
</form>
</FormProvider>
);
};