[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} />;
}