https://react-hook-form.com/
https://github.com/react-hook-form/react-hook-form
- 2025.02 時点で、ほぼデファクトな react form library
- document 豊富で typescript built-in かつ zod resolver も公式提供されてる
- TanStack/form v1 出るまでこれ一択か
- https://react-hook-form.com/get-started
- https://ics.media/entry/240611/
- https://zenn.dev/uzimaru0000/articles/react-hook-form-with-zod
- https://v2.chakra-ui.com/getting-started/with-hook-form
$ npm i react-hook-form @hookform/resolvers
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { userSchema } from 'domain/users' // zod schema
import { Button, FormControl, FormErrorMessage, FormLabel, Input } from '@chakra-ui/react'
const {
register,
handleSubmit,
formState: { errors },
} = useForm({
mode: 'onTouched', // 一度編集して blur したら validation 発火
resolver: zodResolver(userSchema),
})
return <form onSubmit={handleSubmit(d => console.log(d))}>
<FormControl isInvalid={!!errors.username}>
<FormLabel>
ユーザ名
<Input {...register('username', {
// zod 使わないなら validation logic ここに
// required: '必須だよ',
// minLength: { value: 4, message: '4文字以上だよ' },
})} />
</FormLabel>
<FormErrorMessage>
{errors.username?.message}
</FormErrorMessage>
</FormControl>
{/* z.array(z.object({ ... })) のように nest したやつはこんな */}
<FormControl isInvalid={!!errors.contacts?.[0]?.email}>
<FormLabel>
連絡先1
<Input type={'email'} {...register('contacts.0.email')} />
</FormLabel>
<FormErrorMessage>
{errors.contacts?.[0]?.email?.message}
</FormErrorMessage>
</FormControl>
<Button isLoading={isSubmitting} type={'submit'}>
送信
</Button>
</form>
register('', { validate: () => {} })
は zod resolver を使っていると動作しないため、api 返却とか使って動的に validation を追加したい場合はこんなふうに拡張するのが楽そう。
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm({
resolver: zodResolver(
// 追加で superRefine 生やす例
someSchema.superRefine((v, ctx) => {
if (someCondition) {
ctx.addIssue({
code: 'custom',
path: ['somepath'],
message: 'message',
})
}
}),
),
})
- register 以外にも control, Controller を使った細かい実装ができる
- ちょっとでも複雑なやつは control を使うのがよさそう
- 数値を扱う radio や chakra-ui など他 ui lirabry との組み合わせなど
たとえば radio は通常数値を扱えず valueAsNumber が効かない。
https://github.com/orgs/react-hook-form/discussions/4026
これについて control で数値運用を強制しつつ、画像クリックでチェックを切り替えるみたいなことをするならこんな。
import { Controller, useForm } from 'react-hook-form'
const { control } = useForm()
return <>
<Controller
name={'some-radio-field'}
control={control}
render={({ field: { onChange, value } }) => <label>
<input
type={'checkbox'}
onChange={() => onChange(1)}
checked={value === 1}
/>
<div>Some Radio Field Item 1</div>
<img src={'/some-radio-field-image/1.png'} />
</label>}
/>
<Controller
name={'some-radio-field'}
control={control}
render={({ field: { onChange, value } }) => <label>
<input
type={'checkbox'}
onChange={() => onChange(2)}
checked={value === 2}
/>
<div>Some Radio Field Item 2</div>
<img src={'/some-radio-field-image/2.png'} />
</label>}
/>
</>
ちなみに chakra の checkbox なら input を ↓ にすればいい。
<Checkbox
onChange={() => onChange(1)}
isChecked={value === 1}
/>
export const UserEditModal = ({ user, onClose }: {
user: UserSchema
onClose: () => void
}) => {
const { reset, clearErrors } = useForm()
/**
* reset fields on modal open / close
*/
useEffect(() => {
clearErrors()
reset(user)
}, [isOpen])
return <Modal isOpen={isOpen} onClose={onClose}>
</Modal>
}
- https://tech.codmon.com/entry/2024/12/05/013653
- https://qiita.com/someone7140/items/36cfca20adc485f6708a
- https://codesandbox.io/p/sandbox/react-hook-form-usefieldarray-nested-arrays-m8w6j
hasMany な従属 entities を「追加」「削除」するような form で使う。nest したいときは component 分割すればいい。
const { control, register } = useForm()
const { fields, append, remove } = useFieldArray({
control,
name: 'emails',
})
return <>
{fields.map((field, index) => <div key={index}>
<input {...register(`emails.${index}.address`)} />
<button type='button' onClick={() =>remove(index)}>削除</button>
</div>)}
<button type='button' onClick={() =>append({ address: '' })}>
追加
</button>
</>
例えば useFieldArray で追加・削除可能な配列形式で管理しているフィールドで、特定 ID を「ほかフィールドで選択した ID と重複なく選択させる」ため disabled を動的につけたいときに。
const {
control,
register,
watch,
formState: { errors, isSubmitting },
} = useForm({
resolver: zodResolver(UserSchema),
})
const {
fields,
append,
remove,
} = useFieldArray({ control, name: 'roleIds' })
const watches = watch('roleIds') || []
const selectedRoleIds = watches.map(w => w.roleId)
return <>
{/* ... */}
<Select {...register(`specials.${i}.student_id`, { valueAsNumber: true })}>
{ROLES.map(role => <option
key={role.id}
value={role.id}
label={role.name}
disabled={selectedRoleIds.includes(role.id)}
/>)}
</Select>
</>