查閱 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
- 首頁:https://chakra-ui.com
- 文件:https://chakra-ui.com/getting-started
React Hook Form
- 首頁:https://react-hook-form.com
- 文件:https://react-hook-form.com/get-started
接下來的內容
一般用法
使用 <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 的 Radio
、Checkbox
通常不是如 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 文件上有提到該怎麼辦,只是擺在很偏僻的地方,非常容易錯過。
最簡單的方法是用 lodash
的 get()
,就可以把 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>
)}
/>
會發現無法讓 value
是 0
的選項一
預設選取。
需把是數字的 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>