跳至主要内容

[note] React Hook From

react-hook-form @ official website

使用前須知

  • useForm 當成 useState 來思考,所有表單的狀態都會被「保留」在這個 component 內,並且用類似 controlled component 的方式把值帶入 input 欄位中
  • 如果有用到 conditional form,務必要把 reconciliation 處理好,讓 React 能知道這兩個 input 是不同的,否則會造成錯誤(可以透過 key 或 conditional return null,參考這篇: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 的 TouchedDirty 欄位在 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

react-hook-form 傾向使用 uncontrolled components 和瀏覽器原生的表單,但不可避免的有時會需要使用到 controlled components(例如搭配 material UI 或 Antd),因為這些 component 的 input 太深而無法直接使用 register,這時候就可以使用 <Controller /> 這個 wrapper。

寫 component 套用到 react-hook-form 中

  • onBlur() 會影響到 isTouchedisDirty 的值
  • 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 的解構,不能不帶或只帶空物件
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

參考資料
example code (v7)
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 裡的欄位會拿不到(例如,這裡的 relationshipbasicInfo 是在物件的同一層是可以的,但如果想要在 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(),
}),
}),
});

範例程式碼

Sample Code

react-hook-form-yup-when

使用 yup 的 test 來根據多個欄位來驗證特定欄位

keywords: yup, mixed.test(), validation schema by multiple fields

參考 Yup 筆記 @ pjchender

Property 'message' does not exist on type

keywords: NestedValue

在表單中,當欄位的選項是存成 Array 時,例如像 Tag 這類的 Input:

type FormValues = {
tags: string[];
};

如果想要透過 errors.tags.message 取用驗證的錯誤訊息時,會出現類似 Property 'message' does not exist on type' 的錯誤:

Imgur

要解決這個問題可以使用 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;
}

結果:

react-hook-form value with enum

目前不可行:const enum

在 TypeScript 中,還有一個 const enums 這東東,原本以為應該也可以用這個,但測試了一下在 react-hook-form 中也沒辦法用 const enum 來達到預期的效果。

範例程式碼

Sample Code