[note] React Hook From
react-hook-form @ official website
使用前須知
- 把
useForm
當成useState
來思考,所有表單的狀態都會被「保留」在這個 component 內,並且用類似 controlled component 的方式把值帶入 input 欄位中 - 如果有用到 conditional form,務必要把 reconciliation 處理好,讓 React 能知道這兩個 input 是不同的,否則會造成錯誤(可以透過
key
或 conditional returnnull
,參考這篇:React reconciliation: how it works and why should we care) - 如果希望網頁上沒看到這個 input 時,就把對應的欄位和資料都清除的話(更類似 uncontrolled component),則把
shouldUnregister
設為true
(預設是false
,即使 input 被移除,該 input value 仍然會保留在 react hook form 中)。
Nested Object Fields
Nested Object Fields with react-hook-form @ pjchender gist
react-hook-form 有支援使用 nested object fields,只需要使用 .
(dot syntax)就可以了。有幾點需要留意:
- 在 react-hook-form devTools 的 v3.0.0 中,nested object field 的
Touched
和Dirty
欄位在 devtools 中有問題,不會正確顯示,但實際上的 form 是有作用的。 - 如果有搭配 Yup 使用,記得 validation 時,也要把改 object 包在對應的 object field 中。
API
useForm
shouldUnregister
shouldUnregister
預設是 false
,即使 input 被移除,該 input value 仍然會保留在 react hook form 中
- 沒有 render 在 browser 上的欄位其值仍然保留在 react-hook-form 中
- 沒有 render 在 browser 上的欄位不會進行表單驗證
- default Value 在 submission 時會 merge 到送出的表單中
如果 shouldUnregister: true
的話,會更貼近瀏覽器原本表單的行為
- 欄位的值是紀錄在 input 本身上,所以如果該 input 沒有被 render,則該欄位的值就不會被紀錄在 rhf 中
- 只有有被 register 的 input 的資料,才會出現在送出的資料中
reset
reset
是一個非常特別的功能,如果沒有帶入任何參數直接呼叫 reset
的話,會讓表單會到預設值(或是前一次 reset 的值),而不是清空表單的資料。
舉例來說:
const Foo = () => {
// ...
useEffect(() => {
// 把值設成 "Aaron"
reset({
personalName: 'Aaron',
});
}, []);
return (
{/* 使用者點擊 reset,實際上是把表單的值改會 'Aaron' 而不是清空 */}
<button type="button" onClick={() => reset()}>
reset
</button>
);
};
Controller & useController
- 👍 Turn Anything Into A Form Field With React Hook Form Controller @ dev.to:說明使用 Controller 時,為什麼要把
onChange
和value
傳進去,以及如何撰寫客製化的 input component 來套用在 Controller 中。- Controller API @ react-hook-form
react-hook-form 傾向使用 uncontrolled components 和瀏覽器原生的表單,但不可避免的有時會需要使用到 controlled components(例如搭配 material UI 或 Antd),因為這些 component 的 input
太深而無法直接使用 register
,這時候就可以使用 <Controller />
這個 wrapper。
寫 component 套用到 react-hook-form 中
onBlur()
會影響到isTouched
和isDirty
的值onChange()
會影響到value
const YesNoQuestion = ({ selected, onChange, onBlur, title, error }) => {
const handleClick = useCallback(
(value) => () => {
onChange(value);
onBlur(value);
},
[onBlur, onChange],
);
return (
<YesNoQuestionWrapper>
<div className="error">{error}</div>
<div className="title">{title}</div>
<button className={`btn ${selected === false && 'selected'}`} onClick={handleClick(false)}>
否
</button>
<button className={`btn ${selected === true && 'selected'}`} onClick={handleClick(true)}>
是
</button>
</YesNoQuestionWrapper>
);
};
FormProvider & useFromContext
useFormContext API @ react-hook-form
如果表單內的元件很深,又希望不要透過 props 一直將 react-hook-form 的方法透過 props 傳遞到該元件內時,可以在最外層使用 <FormProvider />
,如此在該 Provider 內的子層元件,即可使用 useFormContext
的方式取得 react-hook-form 提供的各種方法。
useFieldArray 搭配 Controller 使用
- DOM Element 上使用的
key
是用 useFieldArray 提供的id
(預設),例如,<li key={item.id}>
- input field 的 name 使用
index
,例如name={`drivers.${index}.firstName` as const}
- remove 的時候使用
index
,例如onClick={() => remove(index)}
,沒有帶 index 的話會全部清掉 - append 的時候,一定要帶入完成 defaultValues 的解構,不能不帶或只帶空物件
- 範例程式碼 @ pjchender codesandbox
- useFieldArray @ react-hook-form
const defaultValues = {
drivers: [
{
firstName: 'Aaron',
},
],
};
export default function App() {
const { handleSubmit, control, formState } = useForm<IDrivers>({
mode: 'onBlur',
resolver: yupResolver(validationSchema),
defaultValues,
});
const { fields, append, prepend, remove, swap, move, insert } = useFieldArray({
control,
name: 'drivers', // 這個 fieldArray 的 unique name
});
const onSubmit: SubmitHandler<IDrivers> = (data) => console.log(data.drivers);
return (
<div className="App">
<form onSubmit={handleSubmit(onSubmit)}>
<button
onClick={() => {
// 一定要帶入完成 defaultValue 的結構,不能不帶或只帶空物件
append({ firstName: 'foo' });
}}
>
Add
</button>
<ul>
{fields.map((item, index) => (
// 這裡要用的是 item.id 不是 index
<li key={item.id}>
<button onClick={() => remove(index)}>Delete</button>
<Controller
name={`drivers.${index}.firstName` as const}
control={control}
// 舊版的 rhf 需要加 defaultValue
// defaultValue={item.firstName}
render={({ field: { onChange, onBlur, value, ref } }) => (
<input
onBlur={onBlur}
onChange={onChange}
value={value}
type="text"
placeholder="username"
ref={ref}
/>
)}
/>
</li>
))}
</ul>
<input type="submit" />
</form>
</div>
);
}
一樣可以搭配 Yup 來做表單驗證:
const validationSchema = yup.object().shape({
drivers: yup
.array()
.of(
yup.object().shape({
firstName: yup.string().required('Required'),
}),
)
.required('必填')
.min(3, '至少要有三個'),
});
TypeScript
register
- Integrating an existing form @ React Hook Form > Get Started
import { InputHTMLAttributes } from 'react';
import { FieldValues, Path, UseFormRegister } from 'react-hook-form';
interface InputProps<T extends FieldValues> extends InputHTMLAttributes<HTMLInputElement> {
label: Path<T>;
register: UseFormRegister<T>;
}
export default function Input<T extends FieldValues>({ label, register, ...props }: InputProps<T>) {
return <input className="w-80 rounded border px-3 py-1" {...register(label)} {...props} />;
}
常見問題
搭配 yup 的 when 根據欄位的值進行動態的規則
keywords: dynamic validation schema with yup
, react-hook-form
react-hook-form 本來就能搭配 Yup 進行表單驗證,但有些時候會需要進行動態的驗證規則,例如,在 A 欄位選了「ooo」才要驗證 B 欄位的「xxx」,具體來說,可能是在「關係」欄位選了其他之後,會多「基本資料」的欄位要使用者填寫,而且要在針對這個多出來的「基本資料」欄位進行表單驗證,這時候使用 yup 提供的 when
方法就非常好用。
有幾個需要留意的地方:
is
後面可以接 function 或直接帶 value
const validationSchema = yup.object().shape({
relationship: yup.string().required(),
// 當 relationship 是 other 的時候,進行特定規則的驗證
// 以這裡來說,就是需要以 "other-" 作為前綴才可以
username: yup.string().when('relationship', {
is: 'other', // 或 (value: string) => value === "other",
then: yup
.string()
.test('username-format', "need to have 'other-' as prefix", (value) => {
return value ? value.startsWith('other-') : true;
})
.required(),
otherwise: yup.string().required(),
}),
});
nested object 也可以使用(參考 Yup when condition inside nested object),但要留意:
when
裡面使用的欄位需要和該欄位在「物件的同一層」,如果是 nested object 裡的欄位會拿不到(例如,這裡的relationship
和basicInfo
是在物件的同一層是可以的,但如果想要在basicInfo.firstName
去拿relationship
的值就會拿不到)
const validationSchema = yup.object().shape({
relationship: yup.string().required(),
// 當 relationship 是 other 是,basicInfo 這個物件需要包含 `firstName` 和 `lastName`
basicInfo: yup.object().when('relationship', {
is: (value: string) => value === 'other',
then: yup.object().shape({
firstName: yup
.string()
.test('firstname-format', "need to have 'other-' as prefix", (value) => {
return value ? value.startsWith('other-') : true;
})
.required(),
lastName: yup
.string()
.test('lastname-format', "need to have 'other-' as prefix", (value) => {
return value ? value.startsWith('other-') : true;
})
.required(),
}),
}),
});
範例程式碼
使用 yup 的 test 來根據多個欄位來驗證特定欄位
keywords: yup
, mixed.test()
, validation schema by multiple fields
參考 Yup 筆記 @ pjchender
Property 'message' does not exist on type
keywords: NestedValue
- Typescript: The property 'message' doesn't exists in 'FieldError | FieldErrors @ Github issue
- NestedValue @ react-hook-form
在表單中,當欄位的選項是存成 Array 時,例如像 Tag 這類的 Input:
type FormValues = {
tags: string[];
};
如果想要透過 errors.tags.message
取用驗證的錯誤訊息時,會出現類似 Property 'message' does not exist on type'
的錯誤:
要解決這個問題可以使用 react-hook-form 提供的 NestedValue
。使用的方式,只需要再定義該欄位的定方,型別使用:
import { NestedValue } from 'react-hook-form';
type FormValues = {
tags: NestedValue<string[]>;
};
如果你用了之後,再定義 defaultValues
的 地方不斷出現類似 Property [$NestedValue]' is missing in type 'never[]' but ...
的錯誤。
這時候可以透過 npm uninstall react-hook-form
再把它重新安裝一下的即可(和 react-hook-form 的版本沒有直接關係,感覺是 package-lock.json
的影響)。
使用變數作為 interface/type 物件型別的 key
keywords: react-hook-form
, key as enum
可行:使用 object as const
直接使用 enum 的話,react-hook-form 的 setValue
會無法正確判斷該欄位的型別,所以需要定義 object 後搭配 as const
:
// ✅ 使用 Object as const
const UserField = {
USERNAME: 'username',
EMAIL: 'email',
} as const;
interface PersonalInfo {
[UserField.USERNAME]: { firstName: string; lastName: string };
[UserField.EMAIL]: string;
}
export default function App() {
// ...
useEffect(() => {
// react-hook-form 才能正確解析 username.firstName 的型別
setValue('username.firstName', 'PJCHENder');
}, [setValue]);
//...
}
不可行:使用 enum
如果使用的是一般的 enum,則會出現如下的錯誤:
Argument of type 'string' is not assignable to parameter of type 'never'.
例如:
// ❌ 使用一般的 enum 在使用 setValue 是會出現錯誤
enum UserField {
USERNAME = 'username',
EMAIL = 'email',
}
interface PersonalInfo {
[UserField.USERNAME]: { firstName: string; lastName: string };
[UserField.EMAIL]: string;
}
結果:
目前不可行:const enum
在 TypeScript 中,還有一個 const enums 這東東,原本以為應該也可以用這個,但測試了一下在 react-hook-form 中也沒辦法用 const enum
來達到預期的效果。