Skip to content

Instantly share code, notes, and snippets.

@yano3nora
Last active March 11, 2025 06:36
Show Gist options
  • Save yano3nora/2b9a65344df040a67fd8d0d65bb28dfe to your computer and use it in GitHub Desktop.
Save yano3nora/2b9a65344df040a67fd8d0d65bb28dfe to your computer and use it in GitHub Desktop.
react-hook-form

Overview

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 出るまでこれ一択か

Usage

$ 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>

Additional validation on ZodResolver

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',
        })
      }
    }),
  ),
})

Controller with control for complex input

https://scrapbox.io/mrsekut-p/react-hook-form%E3%81%A7register%E3%81%A8Controller%E3%81%AE%E3%81%A9%E3%81%A1%E3%82%89%E3%82%92%E4%BD%BF%E3%81%86%E3%81%8B

  • 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}
/>

reset & clearErrors

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>
}

useFieldArray

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>
</>

watch fields

https://react-hook-form.com/docs/useform/watch

例えば 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>
</>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment