React Hook Form (RHF)に入門してみた
基本的な使い方
バリデーションを見越してzod
で型定義します。
import { z } from 'zod';
import { useForm, SubmitHandler } from 'react-hook-form';
const projectSchema = z.object({
projectId: z.number(),
adminUser: z.object({
name: z.string(),
});
});
type Project = z.infer<typeof projectSchema>
const Component: React.FC<{project: Project}> = ({
project
}) => {
const { handleSubmit } = useForm<Project>({
defaultValues: project
});
const onSubmit: SubmitHandler<Project>= data => { console.log(data); };
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input
{...register("projectId")}
/>
<input
{...register("adminUser.name")}
/>
<button type="submit" >Submit</button>
</form>
);
}
Controlled componentとのインテグレーション
RHFはrefを使ってDOMを直接操作することでレンダリングの影響範囲を最小限にとどめています。
しかし私が使用しているコンポーネントは全てuseStateによって宣言的に管理することが前提となっているため、Contoller
を使ってRHFにstateの管理を委ねます。
stateの管理はRHFが担ってくれるので、開発者が意識する必要はありません。
しかもフォームの入力によるレンダリングの影響範囲も最小限にとどめてくれます。
import { z } from 'zod';
import { useForm, SubmitHandler, Controller } from 'react-hook-form';
const projectSchema = z.object({
projectId: z.number(),
adminUser: z.object({
name: z.string(),
});
});
type Project = z.infer<typeof projectSchema>
const Component: React.FC<{project: Project}> = ({
project
}) => {
const { handleSubmit, control } = useForm<Project>({
defaultValues: project
});
const onSubmit: SubmitHandler<Project>= data => { console.log(data); };
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
name={`projectId`}
control={control}
render={({ field }) => {
return (
<MyOwnInput1
value={field.value}
onChange={field.onChange}
/>
);
}}
/>
<Controller
name={`adminUser.name`}
control={control}
render={({ field }) => {
return (
<MyOwnInput2
value={field.value}
onChange={field.onChange}
/>
);
}}
/>
<button type="submit" >Submit</button>
</form>
);
}
zod
でバリデーションを実装する
projectSchema
とzodResolver
を組み合わせてバリデーションを実装します。
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm, SubmitHandler, Controller, SubmitErrorHandler } from 'react-hook-form';
const projectSchema = z.object({
projectId: z.number().min(1),
adminUser: z.object({
name: z.string().nonempty({ message: 'Required' }),
});
});
type Project = z.infer<typeof projectSchema>
const Component: React.FC<{project: Project}> = ({
project
}) => {
const { handleSubmit, control } = useForm<Project>({
defaultValues: project,
resolver: zodResolver(projectSchema),
});
const onSubmit: SubmitHandler<Project>= data => { console.log(data); };
const onError: SubmitErrorHandler<Project>= errors => { console.log(errors); };
return (
<form onSubmit={handleSubmit(onSubmit, onError)}>
<Controller
name={`projectId`}
control={control}
render={({ field, fieldState: { error } }) => {
console.log({ error });
return (
<MyOwnInput1
value={field.value}
onChange={field.onChange}
/>
);
}}
/>
<Controller
name={`adminUser.name`}
control={control}
render={({ field, fieldState: { error } }) => {
console.log({ error });
return (
<MyOwnInput2
value={field.value}
onChange={field.onChange}
/>
);
}}
/>
<button type="submit" >Submit</button>
</form>
);
}
getValues
でフォームの入力値を任意のタイミングで取得する
getValues
を使うと、任意のタイミングでRHFに委任したstateの値を取得できます。
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm, SubmitHandler, Controller, SubmitErrorHandler } from 'react-hook-form';
const projectSchema = z.object({
projectId: z.number().min(1),
adminUser: z.object({
name: z.string().nonempty({ message: 'Required' }),
});
});
type Project = z.infer<typeof projectSchema>
const Component: React.FC<{project: Project}> = ({
project
}) => {
const { handleSubmit, control, getValues } = useForm<Project>({
defaultValues: project,
resolver: zodResolver(projectSchema),
});
const onSubmit: SubmitHandler<Project>= data => { console.log(data); };
const onError: SubmitErrorHandler<Project>= errors => { console.log(errors); };
const onClick = ()=>{ console.log(getValues()) }
return (
<form onSubmit={handleSubmit(onSubmit, onError)}>
<Controller
name={`projectId`}
control={control}
render={({ field, fieldState: { error } }) => {
console.log({ error });
return (
<MyOwnInput1
value={field.value}
onChange={field.onChange}
/>
);
}}
/>
<Controller
name={`adminUser.name`}
control={control}
render={({ field, fieldState: { error } }) => {
console.log({ error });
return (
<MyOwnInput2
value={field.value}
onChange={field.onChange}
/>
);
}}
/>
<button type="submit" onClick={onClick}>Submit</button>
</form>
);
}
useFieldArray
でlist型のフォームを実装する
テーブルなどのlist型のフォームは管理するstateが肥大化してレンダリング処理も重くなりがちですが、useFieldArray
を使うとフォームの入力によるレンダリングの影響範囲も最小限にとどめることができます。
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm, SubmitHandler, Controller, SubmitErrorHandler, useFieldArray } from 'react-hook-form';
const projectSchema = z.object({
projectId: z.number().min(1),
adminUser: z.object({
name: z.string().nonempty({ message: 'Required' }),
});
});
const projectsSchema = z.object({
projects: z.array(projectSchema)
});
type Projects = z.infer<typeof projectsSchema>
const Component: React.FC<{projects: Projects}> = ({
projects
}) => {
const { handleSubmit, control, getValues } = useForm<Projects>({
defaultValues: projects,
resolver: zodResolver(projectsSchema),
});
const { fields } = useFieldArray({
name: 'projects',
control
});
const onSubmit: SubmitHandler<Projects>= data => { console.log(data); };
const onError: SubmitErrorHandler<Projects>= errors => { console.log(errors); };
const onClick = ()=>{ console.log(getValues()) }
return (
<form onSubmit={handleSubmit(onSubmit, onError)}>
<Table>
<TableHead>
<TableRow>
<HeaderCell text={`projectId`} />
<HeaderCell text={`adminName`} />
</TableRow>
</TableHead>
<TableBody>
{fields.map((field, index) => (
<TableRow key={field.id} >
<Controller
name={`projects.${index}.projectId`}
control={control}
render={({ field, fieldState: { error } }) => {
console.log({ index, error });
return (
<MyOwnInput1
value={field.value}
onChange={field.onChange}
/>
);
}}
/>
<Controller
name={`projects.${index}.adminUser.name`}
control={control}
render={({ field, fieldState: { error } }) => {
console.log({ index, error });
return (
<MyOwnInput2
value={field.value}
onChange={field.onChange}
/>
);
}}
/>
</TableRow>
))}
</TableBody>
</Table>
<button type="submit" onClick={onClick}>Submit</button>
</form>
);
};
reset
を使って明示的にuseFormの初期化を行う
例えばSWR
などでデータフェッチをしていると、キャッシュを更新するため親コンポーネントからPropsが複数回降りてくる可能性があります。
しかし、useFormの初期化はコンポーネントの最初のレンダリング時にしか行われません。
そのため、useFormのdefaultValues
にPropsを利用している場合は、reset
を使ってPropsが変更されるたびに明示的にuseFormの初期化をしてあげます。
defaultValues are cached on the first render within the custom hook. If you want to reset the defaultValues, you should use the reset api.
参照
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm, SubmitHandler, Controller, SubmitErrorHandler } from 'react-hook-form';
const projectSchema = z.object({
projectId: z.number().min(1),
adminUser: z.object({
name: z.string().nonempty({ message: 'Required' }),
});
});
type Project = z.infer<typeof projectSchema>
const Component: React.FC<{project: Project}> = ({
project
}) => {
const { handleSubmit, control, getValues, reset } = useForm<Project>({
defaultValues: project,
resolver: zodResolver(projectSchema),
});
useEffect(() => {
reset(project);
}, [reset, project]);
const onSubmit: SubmitHandler<Project>= data => { console.log(data); };
const onError: SubmitErrorHandler<Project>= errors => { console.log(errors); };
const onClick = ()=>{ console.log(getValues()) }
return (
<form onSubmit={handleSubmit(onSubmit, onError)}>
<Controller
name={`projectId`}
control={control}
render={({ field, fieldState: { error } }) => {
console.log({ error });
return (
<MyOwnInput1
value={field.value}
onChange={field.onChange}
/>
);
}}
/>
<Controller
name={`adminUser.name`}
control={control}
render={({ field, fieldState: { error } }) => {
console.log({ error });
return (
<MyOwnInput2
value={field.value}
onChange={field.onChange}
/>
);
}}
/>
<button type="submit" onClick={onClick}>Submit</button>
</form>
);
}
余談:コンポーネントの初期化について
余談ですが、useForm同様、例えばuseStateもコンポーネントの最初のレンダリング時にしか初期化が行われません。そのため、useStateの初期値にPropsの値を利用している場合も、今回同様に初期化をコントロールする必要があります。
上記の例ではuseEffectを使ってuseFormの初期化をおこないましたが、コンポーネントのkeyを利用することも可能です。
Whenever the key (which you’ve set to userId) changes, React will recreate the DOM and reset the state of the Profile component and all of its children. As a result, the comment field will clear out automatically when navigating between profiles
参照
コンポーネントを呼び出す際にkey
を指定する。
const ParentComponent: React.FC<{}> = () => {
const { data } = useFetchNumber();
if(!data) return null;
return (
<ChildComponent key={JSON.stringify(data)} data={data} />
);
}
FormProvider
でコンテキストを作る
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm, SubmitHandler, Controller, SubmitErrorHandler, FormProvider, useFormContext } from 'react-hook-form';
const projectSchema = z.object({
projectId: z.number().min(1),
adminUser: z.object({
name: z.string().nonempty({ message: 'Required' }),
});
});
type Project = z.infer<typeof projectSchema>
const ParentComponent: React.FC<{}> = () => {
const { data: project } = useFetchProjectData();
const { handleSubmit, getValues, control, reset } = useForm<Project>({
defaultValues: project,
resolver: zodResolver(projectSchema),
});
useEffect(() => {
reset(project);
}, [reset, project]);
const onSubmit: SubmitHandler<Project>= data => { console.log(data); };
const onError: SubmitErrorHandler<Project>= errors => { console.log(errors); };
return (
<FormProvider {...{ getValues, control, reset }}>
<form onSubmit={handleSubmit(onSubmit, onError)}>
<ChildComponent project={{ project }} />
</form>
</FormProvider>
);
}
const ChildComponent: React.FC<{project: Project}> = ({
project
}) => {
const { getValues, control, reset } = useFormContext<Project>();
const onClick = ()=>{ console.log(getValues()) }
return (
<>
<Controller
name={`projectId`}
control={control}
render={({ field, fieldState: { error } }) => {
console.log({ error });
return (
<MyOwnInput1
value={field.value}
onChange={field.onChange}
/>
);
}}
/>
<Controller
name={`adminUser.name`}
control={control}
render={({ field, fieldState: { error } }) => {
console.log({ error });
return (
<MyOwnInput2
value={field.value}
onChange={field.onChange}
/>
);
}}
/>
<button type="submit" onClick={onClick}>Submit</button>
</>
);
}
Discussion