react-hook-formのコードを読む2024/11/07
ライブラリ学ぶ手順
- プロジェクトの概要を把握
- 使用例(Usage)の確認
- ソースコードの構造を理解する
- 関数やクラスのシグネチャを確認する
- 具体的な実装を読む
- テストコードを読む
- ドキュメントを読む(思想も含めて)
- useForm
- 基本形なので全て抑える
- useFieldArray
- なぜ[]stringはうまく受け取れないのか(例えば
{ email: string }[]
でないといけないのか)
- なぜ[]stringはうまく受け取れないのか(例えば
- Controll
ソースコードの構造を理解
react-hook-form/
├── app/ # デモ・テスト用アプリケーション
│ ├── src/
│ │ ├── app.tsx # メインアプリ
│ │ ├── basic.tsx # 基本的な使用例
│ │ ├── useFieldArray.tsx # フィールド配列のデモ
│ │ └── useWatch.tsx # Watch機能のデモ
│
├── cypress/ # E2Eテスト
│ ├── integration/ # テストケース
│ └── support/ # テストヘルパー
│
├── src/ # メインのソースコード
│ ├── __tests__/ # ユニットテスト
│ ├── logic/ # コアロジック
│ ├── types/ # 型定義
│ │ ├── form.ts
│ │ ├── fields.ts
│ │ └── path/
│ ├── utils/ # ユーティリティ関数
│ ├── useForm.ts # メインのフォームフック
│ ├── useFieldArray.ts # 配列フィールド用フック
│ └── useWatch.ts # 値監視用フック
│
└── docs/ # ドキュメント
├── README.ja-JP.md # 日本語ドキュメント
└── README.zh-CN.md # 中国語ドキュメント
useForm
export function useForm<
TFieldValues extends FieldValues = FieldValues, // ①
TContext = any, // ②
TTransformedValues extends FieldValues | undefined = undefined, // ③
>(
props: UseFormProps<TFieldValues, TContext> = {}, // ④
): UseFormReturn<TFieldValues, TContext, TTransformedValues> // ⑤
ジェネリックパラメータ
-
TFieldValues
:
- フォームのフィールド値の型を定義
-
FieldValues
(オブジェクト型)を拡張する必要がある - デフォルトは
FieldValues
// 使用例
interface FormInputs {
email: string;
password: string;
}
useForm<FormInputs>() // フォームの型を指定
-
TContext
:
- フォームのコンテキスト情報の型
- デフォルトは
any
- バリデーションコンテキストなどで使用
-
TTransformedValues
:
- 変換後のフォーム値の型
- オプショナル(
undefined
可) - フォーム送信時の値変換に使用
引数とリターン値
-
props: UseFormProps
:
- フォームの設定オプション
- デフォルト値、バリデーションモード、エラーなどを含む
{
defaultValues?: TFieldValues;
errors?: FieldErrors;
disabled?: boolean;
// ...その他の設定
}
-
UseFormReturn
:
- フォームの操作に必要なメソッドと状態を返す
{
register: FieldRegister;
handleSubmit: SubmitHandler;
formState: FormState;
// ...その他のメソッドと状態
}
初期状態の設定
const [formState, updateFormState] = React.useState<FormState<TFieldValues>>({
isDirty: false,
isValidating: false,
isLoading: isFunction(props.defaultValues),
isSubmitted: false,
// ...その他の状態
});
function Form() {
const { handleSubmit, formState: { isSubmitting } } = useForm();
return (
<form onSubmit={handleSubmit(async (data) => {
await submitData(data); // この間isSubmitting = true
})}>
<button disabled={isSubmitting}>
{isSubmitting ? '送信中...' : '送信'}
</button>
</form>
);
}
createSubject.ts
の実装とuseFormとの関係を説明します:
1. Subject(観察者パターン)の基本構造
type Subject<T> = {
readonly observers: Observer<T>[]; // 監視者のリスト
subscribe: (value: Observer<T>) => Subscription; // 監視の登録
unsubscribe: Noop; // すべての監視を解除
} & Observer<T>; // 値の更新機能
2. 主な機能
- subscribe(監視の登録):
const subscribe = (observer: Observer<T>): Subscription => {
_observers.push(observer);
return {
unsubscribe: () => {
_observers = _observers.filter((o) => o !== observer);
},
};
};
- next(値の更新):
const next = (value: T) => {
for (const observer of _observers) {
observer.next && observer.next(value);
}
};
useFormとの関係
useFormでは、このSubjectを以下のような用途で使用しています:
- フォームの状態管理:
it('should unsubscribe to all subject when hook unmounts', () => {
let tempControl: any;
const App = () => {
const { control } = useForm();
tempControl = control;
return null;
};
const { unmount } = render(<App />);
expect(tempControl._subjects.state.observers.length).toBeTruthy();
unmount();
expect(tempControl._subjects.state.observers.length).toBeFalsy();
});
このテストは、フォームのアンマウント時に適切に監視が解除されることを確認しています。
- フィールドの値の監視:
it('should remove input value after input is unmounted with shouldUnregister: true', () => {
const watched: unknown[] = [];
const App = () => {
const [show, setShow] = React.useState(true);
const { watch, register } = useForm({
shouldUnregister: true,
});
watched.push(watch());
return (
<div>
{show && <input {...register('test')} />}
<button
onClick={() => {
setShow(false);
}}
>
toggle
</button>
</div>
);
};
render(<App />);
expect(watched).toEqual([{}]);
fireEvent.change(screen.getByRole('textbox'), {
target: {
value: '1',
},
});
expect(watched).toEqual([
{},
{
test: '1',
},
]);
fireEvent.click(screen.getByRole('button'));
expect(watched).toEqual([
{},
{
test: '1',
},
{
test: '1',
},
{},
]);
});
watch
関数は内部でSubjectを使用して値の変更を監視しています。
- フォーム状態の購読:
/**
* This custom hook allows you to subscribe to each form state, and isolate the re-render at the custom hook level. It has its scope in terms of form state subscription, so it would not affect other useFormState and useForm. Using this hook can reduce the re-render impact on large and complex form application.
*
* @remarks
* [API](https://react-hook-form.com/docs/useformstate) • [Demo](https://codesandbox.io/s/useformstate-75xly)
*
* @param props - include options on specify fields to subscribe. {@link UseFormStateReturn}
*
* @example
* ```tsx
* function App() {
* const { register, handleSubmit, control } = useForm({
* defaultValues: {
* firstName: "firstName"
* }});
* const { dirtyFields } = useFormState({
* control
* });
* const onSubmit = (data) => console.log(data);
*
* return (
* <form onSubmit={handleSubmit(onSubmit)}>
* <input {...register("firstName")} placeholder="First Name" />
* {dirtyFields.firstName && <p>Field is dirty.</p>}
* <input type="submit" />
* </form>
* );
* }
* ```
*/
useFormState
はSubjectを使用してフォームの状態変更を監視します。
実際の使用例
// useForm内部での使用例
function useForm() {
const _subjects = {
watch: createSubject(), // 値の変更監視用
state: createSubject(), // フォーム状態監視用
array: createSubject(), // フィールド配列監視用
};
// コンポーネントのアンマウント時にクリーンアップ
useEffect(() => {
return () => {
_subjects.watch.unsubscribe(); // 監視を解除
_subjects.state.unsubscribe();
_subjects.array.unsubscribe();
};
}, []);
// ...
}
このように、Subjectパターンを使用することで:
- フォームの状態変更を効率的に監視
- 必要なコンポーネントだけを再レンダリング
- メモリリークを防止
- 複雑な状態管理を簡潔に実装
が可能になっています。
// フォームの値を監視するための実装
const createFormSubscription = () => {
const observers = [];
return {
// 値の変更を通知
notifyChange: (newValue) => {
observers.forEach(observer => {
observer.next(newValue);
});
},
// 監視を登録
subscribe: (callback) => {
const observer = {
next: callback
};
observers.push(observer);
// 購読解除用の関数を返す
return () => {
const index = observers.indexOf(observer);
observers.splice(index, 1);
};
}
};
};
// 使用例
const subscription = createFormSubscription();
// 値の変更を監視
const unsubscribe = subscription.subscribe((newValue) => {
console.log('値が変更されました:', newValue);
});
// フォームの値が変更された時
subscription.notifyChange({ field: 'newValue' });
// コンポーネントのアンマウント時
unsubscribe();
useSubscribeでフィールドを登録させるためのnextを追加
import React from 'react';
import { Subject } from './utils/createSubject';
type Props<T> = {
disabled?: boolean;
subject: Subject<T>;
next: (value: T) => void;
};
export function useSubscribe<T>(props: Props<T>) {
const _props = React.useRef(props);
_props.current = props;
React.useEffect(() => {
const subscription =
!props.disabled &&
_props.current.subject &&
_props.current.subject.subscribe({
next: _props.current.next,
});
return () => {
subscription && subscription.unsubscribe();
};
}, [props.disabled]);
}
例
// 1. Subjectの作成
const subject = createSubject();
// 2. useSubscribeでの監視
function FormComponent() {
useSubscribe({
// 監視を無効にするかどうか
disabled: false,
// 監視対象のSubject
subject: subject,
// 値が変更されたときのコールバック
next: (newValue) => {
console.log('値が変更されました:', newValue);
// 必要な処理(状態更新など)
}
});
return <form>...</form>;
}
// 3. 値の更新
subject.next(newValue); // これにより、登録された全てのnext関数が呼び出される
useFormの場合
useSubscribe({
subject: control._subjects.state,
next: (
value: Partial<FormState<TFieldValues>> & { name?: InternalFieldName },
) => {
if (
shouldRenderFormState(
value,
control._proxyFormState,
control._updateFormState,
true,
)
) {
updateFormState({ ...control._formState });
}
},
});
React.useEffect(
() => control._disableForm(props.disabled),
[control, props.disabled],
);
value: Partial<FormState<TFieldValues>> & { name?: InternalFieldName }
の理解
// Partialの定義
type Partial<T> = {
[P in keyof T]?: T[P];
};
// FormStateに適用すると
type PartialFormState = Partial<FormState<TFieldValues>>;
// 具体例
const partialState: PartialFormState = {
isDirty: true,
// すべてのプロパティがオプショナルになる
// isValid, errors などは省略可能
};
// フォームの値の型定義例
type UserFormValues = {
email: string;
password: string;
};
// FormStateの具体例
type UserFormState = FormState<UserFormValues>;
const formState: UserFormState = {
isDirty: false,
isValid: true,
errors: {
email: { type: 'required', message: '必須です' }
},
// ... 他のFormStateプロパティ
};
// 抜粋
export type FormState<TFieldValues extends FieldValues> = {
isDirty: boolean;
isLoading: boolean;
isSubmitted: boolean;
isSubmitSuccessful: boolean;
isSubmitting: boolean;
isValidating: boolean;
isValid: boolean;
disabled: boolean;
submitCount: number;
defaultValues?: undefined | Readonly<DeepPartial<TFieldValues>>;
dirtyFields: Partial<Readonly<FieldNamesMarkedBoolean<TFieldValues>>>;
touchedFields: Partial<Readonly<FieldNamesMarkedBoolean<TFieldValues>>>;
validatingFields: Partial<Readonly<FieldNamesMarkedBoolean<TFieldValues>>>;
errors: FieldErrors<TFieldValues>;
};
フィールドの登録と値の監視の流れ
setされるcreateFormControl
部分
// useFormの内部実装(簡略化)
function useForm() {
const register = (name: string) => {
// フィールドを登録
const field = {
name,
ref: (element: HTMLInputElement) => {
// イベントリスナーの設定
element.addEventListener('input', () => {
const value = element.value;
// 値の変更を通知
_subjects.values.next({
name,
values: { [name]: value }
});
});
element.addEventListener('blur', () => {
// タッチ状態の更新を通知
_subjects.state.next({
name,
touchedFields: { [name]: true }
});
});
}
};
return field;
};
return { register };
}
// フォームの値を更新する関数(setValue)
const setValue = (name: string, value: any) => {
// フォームの値を更新
_formValues[name] = value;
// 監視している対象に通知
_subjects.values.next({
name,
values: _formValues
});
// フォームの状態も更新
_subjects.state.next({
name,
isDirty: true,
dirtyFields: { [name]: true }
});
};
control._subjects.stateの初期化はcreateFormControl関数内で行われています
TFieldValues
の型パラメータはcreateSubject
の型引数として渡され、Subjectが扱う値の型を制限します。
具体的な流れを見てみましょう:
- まず
Subjects
型の定義:
type Subjects<TFieldValues> = {
values: Subject<{
name?: string;
values: TFieldValues;
}>;
array: Subject<{
name?: string;
values?: TFieldValues;
}>;
state: Subject<{
name?: string;
} & Partial<FormState<TFieldValues>>>;
};
-
createSubject
の実装:
export default <T>(): Subject<T> => {
let _observers: Observer<T>[] = [];
const next = (value: T) => {
for (const observer of _observers) {
observer.next(value);
}
};
return {
get observers() {
return _observers;
},
next,
subscribe: (observer: Observer<T>) => {
_observers.push(observer);
return {
unsubscribe: () => {
_observers = _observers.filter((o) => o !== observer);
},
};
},
unsubscribe: () => {
_observers = [];
},
};
};
- 使用例:
const Test = ({ control }: { control: Control<FormValues> }) => {
const { errors } = useFormState({
control,
name: 'firstName',
});
count++;
return <>{errors?.firstName?.message}</>;
};
このコードでは、Control<FormValues>
として型が指定され、その結果:
-
_subjects.values
はFormValues
型の値の変更を通知 -
_subjects.state
はFormState<FormValues>
型の状態変更を通知 -
_subjects.array
は配列操作に関するFormValues
型の変更を通知
このように、TFieldValues
を指定することで、各Subjectが扱うデータの型が適切に制限され、型安全性が確保されます。
useFieldArray
配列要素がオブジェクト構造である必要がある理由は、FieldArrayPath
の型定義に関係しています。
まず、useFieldArray
の型パラメータを見てみましょう:
export type UseFieldArrayProps<
TFieldValues extends FieldValues = FieldValues,
TFieldArrayName extends
FieldArrayPath<TFieldValues> = FieldArrayPath<TFieldValues>,
TKeyName extends string = 'id',
> = {
name: TFieldArrayName;
keyName?: TKeyName;
control?: Control<TFieldValues>;
rules?: {
validate?:
| Validate<FieldArray<TFieldValues, TFieldArrayName>[], TFieldValues>
| Record<
string,
Validate<FieldArray<TFieldValues, TFieldArrayName>[], TFieldValues>
>;
} & Pick<
RegisterOptions<TFieldValues>,
'maxLength' | 'minLength' | 'required'
>;
shouldUnregister?: boolean;
};
ここで重要なのはTFieldArrayName extends FieldArrayPath<TFieldValues>
の部分です。
FieldArrayPath
は配列型のパスを抽出する型で、以下のような構造を想定しています:
// OK: オブジェクト配列
type FormValues = {
emails: { email: string }[]; // ✅ パスは 'emails.0.email'
}
// NG: プリミティブ配列
type FormValues = {
emails: string[]; // ❌ パスは 'emails.0' で終わってしまう
}
これには2つの理由があります:
- パス解決の一貫性:
- オブジェクト配列の場合:
emails.0.email
のように、各要素のプロパティまでパスが解決できる - プリミティブ配列の場合:
emails.0
で終わってしまい、それ以上のパス解決ができない
- フィールド登録の仕組み:
describe('with nested field array ', () => {
type FormValues = {
fieldArray: {
value: string;
nestedFieldArray: {
value: string;
}[];
}[];
};
const ArrayField = ({
arrayIndex,
register,
control,
}: {
arrayIndex: number;
register: UseFormReturn<FormValues>['register'];
arrayField: Partial<FieldValues>;
control: Control<FormValues>;
}) => {
const { fields, append } = useFieldArray({
name: `fieldArray.${arrayIndex}.nestedFieldArray` as const,
control,
rules: {
required: 'This is required',
minLength: {
value: 3,
message: 'Min length of 3',
},
},
});
return (
<div>
{fields.map((nestedField, index) => (
<div key={nestedField.id}>
<input
{...register(
`fieldArray.${arrayIndex}.nestedFieldArray.${index}.value` as const,
)}
/>
</div>
))}
<button
type="button"
onClick={() => {
append({
value:
`fieldArray.${arrayIndex}.nestedFieldArray.${fields.length}.value` as const,
});
}}
>
Add nested array
</button>
</div>
);
};
このテストケースが示すように、各配列要素のプロパティに対して個別のregister
が必要です。プリミティブ配列では、この登録の仕組みが機能しません。
つまり、React Hook Formのフィールドパス解決システムと登録の仕組みが、オブジェクト構造を前提として設計されているためです。
useFieldArray
の実装を見ると、デフォルトのTKeyName
は'id'
として設定されています:
export function useFieldArray<
TFieldValues extends FieldValues = FieldValues,
TFieldArrayName extends
FieldArrayPath<TFieldValues> = FieldArrayPath<TFieldValues>,
TKeyName extends string = 'id',
>(
props: UseFieldArrayProps<TFieldValues, TFieldArrayName, TKeyName>,
): UseFieldArrayReturn<TFieldValues, TFieldArrayName, TKeyName> {
そして、実際のキー生成は以下の部分で行われています:
fields: React.useMemo(
() =>
fields.map((field, index) => ({
...field,
[keyName]: ids.current[index] || generateId(),
})) as FieldArrayWithId<TFieldValues, TFieldArrayName, TKeyName>[],
[fields, keyName],
),
このコードでは、各フィールドに対して:
- デフォルトの
keyName
は'id'
-
ids.current[index]
が存在しない場合はgenerateId()
で新しいIDを生成 - 生成されたIDは
[keyName]: generatedId
の形で各フィールドに追加
実際の使用例を見てみましょう:
// デフォルトの場合(TKeyName = 'id')
const { fields } = useFieldArray({
control,
name: 'test'
});
// fields: [{ id: 'generated-id-1', ...rest }, { id: 'generated-id-2', ...rest }]
// カスタムkeyNameの場合
const { fields } = useFieldArray({
control,
name: 'test',
keyName: 'customId'
});
// fields: [{ customId: 'generated-id-1', ...rest }, { customId: 'generated-id-2', ...rest }]
このユニークなIDは、Reactのリストレンダリングにおけるkey
プロパティとして使用され、配列要素の効率的な更新と再レンダリングを可能にします。
なぜ[]stringはうまく受け取れないのか(例えば{ email: string }[]でないといけないのか
FieldArrayPathがは配列型のパスを抽出する型だが、その各要素はオブジェクト構造を前提としている。
export type FieldArrayPath<TFieldValues extends FieldValues> =
ArrayPath<TFieldValues>;
オブジェクト構造だからこそ、スプレッド演算子(...field
)を使って既存のフィールドを展開し、新しいプロパティ([keyName]: id
)を追加できるがプリミティブだと機能しない。
例えば:
// ✅ オブジェクト配列の場合
const field = { name: 'John' };
const fieldWithId = {
...field,
id: 'generated-id'
}; // { name: 'John', id: 'generated-id' }
// ❌ プリミティブ配列の場合
const field = 'John';
const fieldWithId = {
...field,
id: 'generated-id'
}; // スプレッド演算子が機能しない
これはuseFieldArray
の実装の中で重要な部分です:
fields: React.useMemo(
() =>
fields.map((field, index) => ({
...field,
[keyName]: ids.current[index] || generateId(),
})) as FieldArrayWithId<TFieldValues, TFieldArrayName, TKeyName>[],
[fields, keyName],
),
このコードブロックは、各フィールドに一意のIDを付与する処理を行っていますが、これはフィールドがオブジェクト構造であることを前提としています。プリミティブ値の場合、このような拡張ができないため、フィールドの識別や追跡が困難になります。
TypeScriptの型システムと実際のJavaScriptの実行時の動作は異なるためです。
実際の実行時には:
// プリミティブ値でも、JavaScriptは以下のような変換を試みます
const field = "John";
const fieldWithId = {
...field, // プリミティブ値をオブジェクトとして展開しようとする
id: 'generated-id'
}
// 結果:
console.log(fieldWithId); // { id: 'generated-id' }
// 元の値"John"は失われる
このため:
- プログラムは実行時エラーにはならない
- しかし、期待した動作(元の値の保持)はできない
実際のuseFieldArray
の使用例を見てみると:
{fields.map((item, i) => (
<Controller
key={item.id} // ← idは存在するが
render={({ field }) => <input {...field} aria-label={'name'} />}
name={`test.${index}.keyValue.${i}.name`} // ← 元の値にアクセスできない
control={control}
/>
))}
このコードは:
-
item.id
は存在する(追加されたため) - しかし元のフィールドの値は失われている
という状態で動作することになります。
つまり、TypeScriptの型チェックでは捕捉できるエラーですが、実行時には"静かに失敗"する形になります。これは特にデバッグを困難にする可能性があるため、型システムで防ぐことが推奨されています。
これはcontrol._getFieldArray(name)
の結果によるものです。このメソッドは、フォームの値から指定されたname
のパスに対応する配列を取得します。
例えば:
// フォームの値
{
test: [
{ email: "test1@example.com" },
{ email: "test2@example.com" }
]
}
// useFieldArrayの呼び出し
const { fields } = useFieldArray({
name: "test",
control
});
// fieldsの中身
[
{ id: "generated-1", email: "test1@example.com" },
{ id: "generated-2", email: "test2@example.com" }
]
これはuseFieldArray
内部で以下のような処理が行われているためです:
const remove = (index?: number | number[]) => {
const updatedFieldArrayValues: Partial<
FieldArrayWithId<TFieldValues, TFieldArrayName, TKeyName>
>[] = removeArrayAt(control._getFieldArray(name), index);
ids.current = removeArrayAt(ids.current, index);
updateValues(updatedFieldArrayValues);
setFields(updatedFieldArrayValues);
control._updateFieldArray(name, updatedFieldArrayValues, removeArrayAt, {
argA: index,
});
};
control._getFieldArray(name)
は配列内のオブジェクトの構造をそのまま保持したまま返し、それにid
が付与される形になります。つまり、元の配列の各要素のプロパティ(この場合はemail
)は保持されたままです。
Controller, control
このコードは、React を使用してフォームを作成し、react-hook-form と React DatePicker を組み合わせて日付選択機能を実装しています。特に、Controller
コンポーネントの render
関数部分についての疑問があるとのことなので、詳しく解説します。
全体の流れ
-
必要なライブラリのインポート
import ReactDatePicker from "react-datepicker" import { TextField } from "@material-ui/core" import { useForm, Controller } from "react-hook-form"
-
フォームの値の型定義
type FormValues = { ReactDatepicker: string }
-
フォームコンポーネントの定義
function App() { const { handleSubmit, control } = useForm<FormValues>() return ( <form onSubmit={handleSubmit((data) => console.log(data))}> <Controller control={control} name="ReactDatepicker" render={({ field: { onChange, onBlur, value, ref } }) => ( <ReactDatePicker onChange={onChange} // フォームの状態を更新 onBlur={onBlur} // フォームのタッチ状態を通知 selected={value} // フォームの現在の値を設定 /> )} /> <input type="submit" /> </form> ) }
詳細な解説
useForm
フックの使用
1. const { handleSubmit, control } = useForm<FormValues>()
-
useForm
は react-hook-form のフックで、フォームの状態管理を行います。 -
handleSubmit
はフォーム送信時の処理をラップする関数です。 -
control
はフォームコントロールを管理するオブジェクトで、Controller
コンポーネントに渡します。
Controller
コンポーネント
2. <Controller
control={control}
name="ReactDatepicker"
render={({ field: { onChange, onBlur, value, ref } }) => (
<ReactDatePicker
onChange={onChange}
onBlur={onBlur}
selected={value}
/>
)}
/>
Controller
は、react-hook-form がネイティブにサポートしていないカスタムコンポーネント(この場合は ReactDatePicker)をフォームに統合するためのコンポーネントです。
-
control
:useForm
から取得したコントロールオブジェクトを渡します。 -
name
: フォームデータ内のこのフィールドのキー名を指定します。 -
render
: 実際にフォームフィールドをレンダリングするための関数を提供します。
render
関数の内部
3. render={({ field: { onChange, onBlur, value, ref } }) => (
<ReactDatePicker
onChange={onChange} // フォームの状態を更新
onBlur={onBlur} // フォームのタッチ状態を通知
selected={value} // フォームの現在の値を設定
/>
)}
render
関数は、Controller
が管理するフィールドに必要なプロパティを ReactDatePicker に渡すために使用されます。この関数の引数として渡されるオブジェクトには、フィールドの状態やイベントハンドラーが含まれています。ここでは、以下のプロパティを ReactDatePicker に渡しています:
-
onChange
: ユーザーが日付を選択したときに呼び出され、フォームの状態を更新します。 -
onBlur
: フィールドがフォーカスを失ったときに呼び出され、フォームがフィールドの「タッチ」状態を認識します。 -
value
: 現在のフィールドの値を ReactDatePicker に渡します。 -
ref
: フィールドの参照ですが、この例では使用されていません。
これにより、ReactDatePicker が react-hook-form のフォーム状態と連携し、選択された日付がフォームデータとして管理されます。
4. フォームの送信
<form onSubmit={handleSubmit((data) => console.log(data))}>
...
<input type="submit" />
</form>
-
handleSubmit
がフォームの送信を処理し、バリデーションを行った後、データをコンソールに出力します。
その他のポイント
-
TextField
のインポートについて:import { TextField } from "@material-ui/core"
このコードでは
TextField
がインポートされていますが、実際には使用されていません。不要であれば削除することをお勧めします。 -
型の一致について:
type FormValues = { ReactDatepicker: string }
ReactDatePicker は通常、
Date
オブジェクトを扱いますが、ここではstring
型として定義されています。実際の使用においては型の不一致が問題になる可能性があるため、必要に応じて型を調整してください。例えば、以下のように
Date
型に変更することができます:type FormValues = { ReactDatepicker: Date }
まとめ
-
Controller
コンポーネントは、react-hook-form とカスタムコンポーネント(この場合は ReactDatePicker)を連携させるために使用されます。 -
render
関数内で渡されるfield
のプロパティ(onChange
,onBlur
,value
,ref
)は、react-hook-form が管理するフォームの状態とイベントをカスタムコンポーネントに適切に伝えるためのものです。 - これにより、ReactDatePicker は react-hook-form の一部として動作し、フォームの状態管理やバリデーションがシームレスに行われます。
このようにして、react-hook-form を使用することで、フォームの状態管理が簡潔かつ効率的に行えるようになります。
controller.tsにて、使えるpropがわかる
//controller.ts
*/
export type ControllerProps<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
render: ({
field,
fieldState,
formState,
}: {
field: ControllerRenderProps<TFieldValues, TName>;
fieldState: ControllerFieldState;
formState: UseFormStateReturn<TFieldValues>;
}) => React.ReactElement;
} & UseControllerProps<TFieldValues, TName>;
ControllerRenderProps<TFieldValues, TName>
の型パラメータについて説明します。
例えば、以下のコードの場合:
type FormValues = {
test: {
firstName: string;
lastName: string;
keyValue: { name: string }[];
}[];
};
このようなFormValues
型があるとき、Controller
コンポーネントの使用箇所では:
{fields.map((item, i) => (
<Controller
key={item.id}
render={({ field }) => <input {...field} aria-label={'name'} />}
name={`test.${index}.keyValue.${i}.name`}
control={control}
/>
))}
ここでのTFieldValues
とTName
は以下のように解決されます:
-
TFieldValues
=FormValues
型:
{
test: {
firstName: string;
lastName: string;
keyValue: { name: string }[];
}[];
}
-
TName
=test.${index}.keyValue.${i}.name
という文字列リテラル型
これにより、ControllerRenderProps
の各プロパティは以下のように具体化されます:
{
onChange: (...event: any[]) => void;
onBlur: () => void;
value: string; // keyValue[i].nameの型
name: `test.${number}.keyValue.${number}.name`;
ref: RefCallBack;
disabled?: boolean;
}
この型パラメータにより、フォームの値の型安全性が保証され、特にvalue
の型が正しく推論されます。
以下の流れで値が更新されます:
-
useController
フックでfield
オブジェクトが生成されます:
onChange: React.useCallback(
(event) =>
_registerProps.current.onChange({
target: {
value: getEventValue(event),
name: name as InternalFieldName,
},
type: EVENTS.CHANGE,
}),
[name],
),
- このonChangeハンドラは
_registerProps.current.onChange
を呼び出し、これはcontrol.register
によって提供されます:
const _registerProps = React.useRef(
control.register(name, {
...props.rules,
value,
...(isBoolean(props.disabled) ? { disabled: props.disabled } : {}),
}),
);
-
<input {...field}
で展開されると、以下のプロパティが入力要素に適用されます:
export type ControllerRenderProps<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
onChange: (...event: any[]) => void;
onBlur: Noop;
value: FieldPathValue<TFieldValues, TName>;
disabled?: boolean;
name: TName;
ref: RefCallBack;
};
つまり:
- ユーザーが入力を行う
- inputのonChangeイベントが発火
-
field.onChange
が呼び出される -
_registerProps.current.onChange
が実行され、フォームの値が更新される -
useWatch
やuseFormState
などを通じて、更新された値が反映される
このプロセスは全て自動的に行われ、開発者が明示的にハンドリングする必要はありません。
フォームのステート管理