はじめに
<input> や <textarea> はreact-hook-formの register() で簡単に管理できますが、
ReactSelect などのカスタムUIコンポーネントには使えません。
そこで Controller を使います。
Controllerとは
Controller は、カスタムUIコンポーネントをreact-hook-formで管理するためのラッパーです。
<Controller
name="skill_ids" // フォームのフィールド名
control={control} // useFormが返す管理オブジェクト
rules={{ validate: (v) => v.length > 0 || "選択してください" }}
render={({ field }) => (
<ReactSelect
onChange={field.onChange}
value={field.value}
/>
)}
/>
controlとは
useForm() を呼び出すと返ってくる管理オブジェクトです。
const {
register,
handleSubmit,
control, // ← これ
formState: { errors },
} = useForm<FormData>();
フォーム全体の状態(各フィールドの値・バリデーション状態など)を保持しており、
Controller に渡すことで「このフォームに属してください」と伝えます。
fieldとは
render の引数として Controller が渡してくれるオブジェクトです。
| プロパティ | 役割 |
|---|---|
| field.value | フォームが持つ現在の値 |
| field.onChange | 値が変わったときフォームに伝える関数 |
| field.onBlur | フォーカスが外れたときフォームに伝える関数 |
renderとは
Controller が「何を描画するか」を指定するプロパティです。
関数を渡すと、Controller がその関数を呼び出してUIを描画します。
<Controller
name="skill_ids"
control={control}
render={({ field }) => ( // ← ここに描画したいコンポーネントを書く
<ReactSelect ... />
)}
/>
ReactSelectとは
クリックだけで複数選択できるドロップダウンUIライブラリです。
npm install react-select
import ReactSelect from "react-select";
<ReactSelect
isMulti
options={[
{ label: "React", value: 1 },
{ label: "TypeScript", value: 2 },
]}
onChange={(selected) => console.log(selected)}
/>
options は必ず { label, value } 形式で渡します。
ControllerとReactSelectを組み合わせる
// フォームの型定義
type FormData = {
skill_ids: number[];
};
const {
register,
handleSubmit,
reset,
control,
formState: { errors },
} = useForm<FormData>({
defaultValues: {
skill_ids: [],
},
});
//〜〜〜〜〜〜〜〜〜〜〜〜〜〜省略
<Controller
name="skill_ids"
control={control}
rules={{
validate: (v) => v.length > 0 || "好きな技術を選択してください",
}}
render={({ field }) => (
<ReactSelect
isMulti
options={skills?.map((s) => ({ label: s.name, value: s.id }))}
// フォームの number[] → ReactSelectの {label, value}[] に変換
value={skills
?.filter((s) => field.value.includes(s.id))
.map((s) => ({ label: s.name, value: s.id }))}
// ReactSelectの {label, value}[] → フォームの number[] に変換
onChange={(selected) => {
field.onChange(selected.map((s) => s.value));
}}
/>
)}
/>
選択から表示までの流れ
- 選択肢をクリック
↓ - onChange が呼ばれる
field.onChange([1, 2])
↓ - react-hook-form の内部状態が更新される
↓ - 再レンダリングが発生する
↓ - 新しい field.value が render に渡される
field.value = [1, 2]
↓ - value が再計算される
skills.filter((s) => [1, 2].includes(s.id))
→ [{ label: "React", value: 1 }, { label: "TypeScript", value: 2 }]
↓ - ReactSelect に選択済み項目として表示される