Chakra UI 搭配 React-Hook-Form,文件上沒說清楚的地方

查閱 Chakra UI 和 React Hook Form 的文件,已經能夠滿足 8 成的使用情況,但是,在建立表單元件 <Radio /><Checkbox />,就沒那麼順利了,在文件上找不到如何搭配使用。

使用環境

"next": "^13.1.1",
"@chakra-ui/react": "^2.7.1",
"react-hook-form": "^7.45.1"

Chakra UI 的標誌

Chakra UI

React Hook Form 的標誌

React Hook Form


接下來的內容


一般用法

使用 <Input /> 的表單,只要按照 React Hook Form 的說明,加上 register() 即可:

import { useForm } from 'react-hook-form';
import { Input } from '@chakra-ui/react';

const { register } = useForm({ defaultValues });

<Input {...register('name', { required: true })} />

文件上說明得很清楚,就不再贅述。


Radio 和 Checkbox 的用法

Chakra UI 的 RadioCheckbox 通常不是如 html 表單直接使用,而是有語法規則:

import { RadioGroup, Radio, CheckboxGroup, Checkbox, Stack } from '@chakra-ui/react';

<RadioGroup>
  <Stack>
    <Radio>選項一</Radio>
    <Radio>選項二</Radio>
    <Radio>選項三</Radio>
  </Stack>
</RadioGroup>

<CheckboxGroup>
  <Stack>
    <Checkbox>選項一</Checkbox>
    <Checkbox>選項二</Checkbox>
    <Checkbox>選項三</Checkbox>
  </Stack>
</CheckboxGroup>

試過很多方法,卻總是在與 defaultValues 相關的情境不如預期。

// ❌ 會出現錯誤的語法

// 在 Group 層級加上 register()

<RadioGroup {...register('range')}>
  <Stack>
    ...
  </Stack>
</RadioGroup>

<CheckboxGroup {...register('option')}>
  <Stack>
    ...
  </Stack>
</CheckboxGroup>

// 或在選項加上 register()

<RadioGroup>
  <Stack>
    <Radio {...register('range')} value={0}>
      選項一
    </Radio>
    <Radio {...register('range')} value={1}>
      選項二
    </Radio>
    <Radio {...register('range')} value={2}>
      選項三
    </Radio>
  </Stack>
</RadioGroup>

<CheckboxGroup>
  <Stack>
    <Checkbox {...register('option')} value={0}>
      選項一
    </Checkbox>
    <Checkbox {...register('option')} value={1}>
      選項二
    </Checkbox>
    <Checkbox {...register('option')} value={2}>
      選項三
    </Checkbox>
  </Stack>
</CheckboxGroup>

正確的方法是使用 React Hook Form 的 Controller

// ✅ 正確的語法

import { useForm, Controller } from 'react-hook-form';
import { RadioGroup, Radio, CheckboxGroup, Checkbox, Stack } from '@chakra-ui/react';

const { control } = useForm({ defaultValues });

<Controller
  name="range" //改用 name
  control={control}
  render={({ field }) => (
    <RadioGroup {...field}>
      <Stack>
        <Radio value={0}>選項一</Radio>
        <Radio value={1}>選項二</Radio>
        <Radio value={2}>選項三</Radio>
      </Stack>
    </RadioGroup>
  )}
/>

<Controller
  name="option" //改用 name
  control={control}
  render={({ field }) => (
    <RadioGroup {...field}>
      <Stack>
        <Radio value={0}>選項一</Radio>
        <Radio value={1}>選項二</Radio>
        <Radio value={2}>選項三</Radio>
      </Stack>
    </RadioGroup>
  )}
/>

name 和錯誤訊息的資料格式

React Hook Form 設定表單物件層級的方法是在 register() 裡面,以 . 分開的字串;而錯誤訊息是物件。

這個不同之處,會在表單有許多欄位,且層級不同時出現問題:

const { register, formState: { errors } } = useForm({ defaultValues });
import { FormControl, Input, FormErrorMessage } from '@chakra-ui/react';

const fields = [
  {
    name: 'displayName',
    rules: {
      required: {
        value: true,
        message: '必填'
      }
    }
  },
  {
    name: 'subscription.months',
    rules: {
      max: {
        value: 12,
        message: '最多 12 個月'
      }
    }
  },
  {
    name: 'company.id',
    rules: {
      maxLength: {
        value: 20,
        message: '最多 20 個字'
      }
    }
  }
];

// ...

{fields.map(({ name, rules }) =>
  // ❌ 只有第 1 組欄位會在不符規則時,顯示錯誤訊息
  <FormControl isInvalid={errors[name]}>
    <Input {...register(name, { ...rules })} />
    <FormErrorMessage>{!!errors[name] && errors[name].message}</FormErrorMessage>
  </FormControl>
)}

其實 React Hook Form 文件上有提到該怎麼辦,只是擺在很偏僻的地方,非常容易錯過。

最簡單的方法是用 lodashget(),就可以把 subscription.months 之類的格式變成物件:

import get from 'lodash/get';

//...

{fields.map(({ name, rules }) => {
  // ✅ 取得 errors 物件裡,對應的 value 和 message
  const error = get(errors, name);

  return (
    <FormControl isInvalid={error}>
      <Input {...register(name, { ...rules })} />
      <FormErrorMessage>{!!error && error.message}</FormErrorMessage>
    </FormControl>
  )}
)}

可能會沒注意到的地方

型別

React Hook Form 儲存資料的格式一律是字串。

所以在使用 <Radio /><Checkbox /> 的時候,若把 value 值指定為數字,並設定 defaultValue

// ⚠️ 選項一應要預設選取
import { useForm, Controller } from 'react-hook-form';
import { RadioGroup, Radio, Stack } from '@chakra-ui/react';

const { control } = useForm({ defaultValues });

<Controller
  name="range"
  control={control}
  render={({ field }) => (
    <RadioGroup {...field} defaultValue={0}>
      <Stack>
        <Radio value={0}>選項一</Radio>
        <Radio value={1}>選項二</Radio>
        <Radio value={2}>選項三</Radio>
      </Stack>
    </RadioGroup>
  )}
/>

會發現無法讓 value0選項一預設選取。

需把是數字的 defaultValue 轉為字串:

// ✅ 正確的語法(之一)

defaultValue={`${0}`}

驗證

React Hook Form 所控制的表單,可以使用 validate 來驗證輸入的資料是否有效:

// ...
<FormControl isInvalid={error}>
  <Input {...register('min', {
    validate: {
      minNumber: (value) => !isNaN(value) && value > 0 || '必須大於 0'
    }
  })} />
</FormControl>

在複雜的表單裡,常常會需要根據另一個欄位,來驗證欄位。例如:有最小值、最大值欄位,而最大值必須大於最小值。

先前爬文到這樣做:

// ...
<FormControl isInvalid={error}>
  <Input {...register('max', {
    validate: {
      maxNumber: (value) => !isNaN(value) && (value > getValues('min')) || '必須大於最小值'
    }
  })} />
</FormControl>

然而,在結構複雜的專案裡,會遇到不方便直接使用 getValues() 的情境。

其實文件上確實有提到要怎麼做,但沒有進一步解釋。

方法是在 validate 可以再傳入 formValues

// ...
<FormControl isInvalid={error}>
  <Input {...register('max', {
    validate: {
      maxNumber: (value, formValues) =>
        !isNaN(value) && (value > formValues?.min) || '必須大於最小值';
    }
  })} />
</FormControl>

💬 討論

新增討論 👆連至 Github 的 Discussions 參與