ワスド株式会社 開発チームのmako_jiiです。 今回は「react-hook-formのSubmitHandlerを利用したonSubmit処理の返り値に型をつける」と題しまして、記事を書かせていただこうかと思います。
なぜこの記事を書こうと思ったかというと、react-hook-form
を使用した実装中で詰まったところがあったからです。
下記画像のように、useForm
を使用する場合form
部分のみ別コンポーネントに切り離して実装することが弊社では多いです。
form
の状態管理にはreact-hook-form
を使用しており、バリデーションにはclass-validator
を使用しています。
// Settings.tsx import React from "react"; import { SettingsForm, OnSubmitSettingsFormValues } from "components/SettingsForm"; const Settings: React.FC = () => { const onSubmit: OnSubmitSettingsFormValues = async (data) => { await updateSettings(data); // DB更新処理など }; return ( <SettingsForm onSubmit={onSubmit} /> ); };
// SettingsForm.tsx import React from "react"; import { useForm, SubmitHandler } from "react-hook-form"; class SettingsFormValues { @IsNotEmpty({ message: "住所を入力してください" }) address: string; @IsNotEmpty({ message: "名前を入力してください" }) name: string; } export type OnSubmitSettingsFormValues = SubmitHandler<SettingsFormValues>; export type SettingsFormProps = { onSubmit: OnSubmitSettingsFormValues; }; const SettingsForm: React.FC<SettingsFormProps> = ({ onSubmit }) => { const { register, control, handleSubmit } = useForm<SettingsFormValues>(); return ( <form onSubmit={handleSubmit(onSubmit)}> {/* テキストボックスとか */} </form> ); };
useForm
やform
周りにまつわる部分のみ別コンポーネントに切り離しますが、
form
で入力した内容に基づいたDBの更新処理などはメインのコンポーネントに置いておいて、引数でその関数を渡しています。
更新処理の関数の型は、form
のコンポーネントで作成した型にしております。
form
の値が変更された時のみ保存ボタンを活性化させるために、DBに保存してある値でform
の初期値を設定することに加え、
保存後に保存ボタンを非活性化させるために、保存後の値でform
の初期値をreset
させる実装をしました。
// Settings.tsx // onSubmit処理のみ記載 const onSubmit: OnSubmitSettingsFormValues = async (data) => { const { address, name } = await updateSettings(data); // DB更新処理など return { address, name }; // 返り値でオブジェクトを返す };
// SettingsForm.tsx // importや型定義は省略 const SettingsForm: React.FC<SettingsFormProps> = ({ onSubmit }) => { const { register, control, reset, handleSubmit } = useForm<SettingsFormValues>(); // SettingsForm内でラップする const _onSubmit: OnSubmitSettingsFormValues = async (data) => { const result = await onSubmit(data); reset(result); // anyを渡してしまい危険なコードになる }; return ( <form onSubmit={handleSubmit(_onSubmit)}> {/* テキストボックスとか */} </form> ); };
がしかし、 コード内にも書いてあるとおりany
がreset
に渡ってしまい危険なコードになってしまいます。
なんでかというとreact-hook-form
のSubmitHandler
がそういう型定義をしているからです。
なんとか型推論させて安全にしてあげたい、
いろいろな解決方法があるかと思いますが、今回はプロパティの定義にclass
を使用したためinstanceof
を使って型を判定させることにしました。
// Settings.tsx // onSubmit処理のみ記載 const onSubmit: OnSubmitSettingsFormValues = async (data) => { const { address, name } = await updateSettings(data); // DB更新処理など return { address, name }; // 返り値でオブジェクトを返す };
// SettingsForm.tsx // _onSubmit処理のみ記載 // SettingsForm内でラップする const _onSubmit: OnSubmitSettingsFormValues = async (data) => { const result = await onSubmit(data); // resultはオブジェクトであってインスタンスではないので、このif文はfalseになってしまう if(result instanceof SettingsFormValues) { reset(result); } };
SettingFormValues
と同じ形のオブジェクトを渡して、型判定をinstanceof
で行うようにコーディングしてみましたが、
コメントにも記載したようにif文の判定がfalseになってしまい意図した動作をしません。
調べた結果、値を返す時にオブジェクトを返しているのでSettingFormValues
クラスのインスタンスではないと判定されることが原因と分かりました。
そこで、プロパティを定義したclass
にconstructor
を追加し初期値を載っけられるようにして、
// SettingsForm.tsx // 処理なのは変更なしのため省略 export class SettingsFormValues { @IsNotEmpty({ message: "住所を入力してください" }) address: string; @IsNotEmpty({ message: "名前を入力してください" }) name: string; constructor(address:string, message:string) { this.address = addess; this.message = message; } }
return new
で返したい値をSettingFormValues
のインスタンスで返すようにしました。
// Settings.tsx // onSubmit処理のみ記載 const onSubmit = () => { const result = UpdateDBProcess(); // 更新処理 return new SettingsFormValues(result.address, result.name); };
うまくいきました!
返り値を扱っている時はany
ですが、ifブロック内でresultは型推論されている状態で使用することができます。
以下最終形態です。
// Settings.tsx import React from "react"; import { SettingsForm, OnSubmitSettingsFormValues, SettingsFormValues, } from "components/SettingsForm"; const Settings: React.FC = () => { const onSubmit: OnSubmitSettingsFormValues = async (data) => { const { address, name } = await updateSettings(data); // DB更新処理など return new SettingsFormValues(address, name); // 返り値でインスタンスを返す }; return ( <SettingsForm onSubmit={onSubmit} /> ); };
// SettingsForm.tsx import React from "react"; import { useForm, SubmitHandler } from "react-hook-form"; // Settings.tsxで使うのでexport export class SettingsFormValues { @IsNotEmpty({ message: "住所を入力してください" }) address: string; @IsNotEmpty({ message: "名前を入力してください" }) name: string; // コンストラクタを定義 constructor(address: string, name: string) { this.address = address; this.name = name; } } export type OnSubmitSettingsFormValues = SubmitHandler<SettingsFormValues>; export type SettingsFormProps = { onSubmit: OnSubmitSettingsFormValues; }; const SettingsForm: React.FC<SettingsFormProps> = ({ onSubmit }) => { const { register, control, handleSubmit } = useForm<SettingsFormValues>(); // SettingsForm内でラップする const _onSubmit: OnSubmitSettingsFormValues = async (data) => { const result = await onSubmit(data); if(result instanceof SettingsFormValues) { reset(result); } }; return ( <form onSubmit={handleSubmit(_onSubmit)}> {/* テキストボックスとか */} </form> ); };
あるいは別解として、class-transformerというライブラリ中のplainToInstance関数を利用して、オブジェクトをクラスに変換することでも正しく動作します。
こちらの方がコード量も少なく、よりよい実装かもしれません。
この場合、class
のconstructor
は不要となります。
// Settings.tsx // onSubmit処理のみ記載 const onSubmit: OnSubmitSettingsFormValues = async (data) => { const { address, name } = await updateSettings(data); // DB更新処理など return { address, name }; };
// SettingForms.tsx // _onSubmit処理のみ記載 // SettingsForm内でラップする const _onSubmit: OnSubmitSettingsFormValues = async (data) => { const result = await onSubmit(data); const values = plainToInstance(SettingsFormValues, result); if(values instanceof SettingsFormValues) { reset(result); } };
かなり具体的なユースケースで発生した問題ですが、紐解いてみると解決にはTypeScriptのしっかりとした実力が求められ、改めて勉強になりました。
今後も、WASD TECH BLOGでは開発チームの取り組みや、 React, TypeScript, GraphQL, Hasura, AWS といった技術スタックについての記事を定期的に発信していきます。よろしければ過去記事や、 もよろしくお願いいたします。
以上、mako_jiiでした。