はじめに
普段はスタートアップで建設業界向けのSaaSの開発をしているtaroと申します。
今回はReact18で登場したSuspenseを触っていたら、React Hook Formでフォームの初期値に非同期な値を設定するのが簡単になりそうだなーって思ったので、試してみました。
React Hook Formを使ったことがない方でもきっとわかるような内容になっていると思うので、ぜひぜひ読んでみてくださいー!
この記事はこちらのイベントに参加しています。
前提を揃えるためにReact Hook Formを少し復習
本題に入る前に、React Hook Formについて少し復習して前提を揃えていこうと思います。
(「復習はいらないよー!」って方は、React Hook FormでSuspenseを使うまで飛んでください!)
またSuspenseについては、公式ドキュメントや別の記事等をご参考ください。
特にuhyoさんの記事がとてもわかりやすくまとまっているのでおすすめです!
React Hook Formとは?
React Hook FormはHooksベースでFormを管理できるのが特徴で、Reactのフォーム管理だと一位を争うほどよく採用されているライブラリです。
簡単なフォームを作ってみる
今回はReact Hook Formを使って、名前と年齢を入力してコンソールに出力する簡単なフォームを作ってみます。
フォームのComponentはこちらです。
import { FC } from "react";
import { useForm } from "react-hook-form";
type Inputs = {
name: string;
age: number;
};
const Form: FC = () => {
const { register, handleSubmit } = useForm<Inputs>();
const onSubmit = (data: Inputs) => console.log(data);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("name")} type="text" placeholder="氏名" />
<input {...register("age")} type="number" placeholder="年齢" />
<input type="submit" />
</form>
);
};
このフォームでReact Hook Form特有の書き方をしている場所は3箇所です。
useForm
を呼び出してフォームを初期化し、フォームを管理するためのメソッドを受け取る
const { register, handleSubmit } = useForm<Inputs>();
register
で各input
をReact Hook Formの管理下に登録する
React Hook Formではname
属性(今回はname
とage
)を使って、各input
を識別して管理します。
<input {...register("name")} type="text" placeholder="氏名" />
<input {...register("age")} type="number" placeholder="年齢" />
handleSubmit
でReact Hook Formで管理しているフォームのデータを受け取る
handleSubmit
でラップした関数(onSubmit
)には、Validateされたフォームのデータが渡されます。
const onSubmit = (data: Inputs) => console.log(data);
return (
<form onSubmit={handleSubmit(onSubmit)}>
フォームに初期値を設定する
次にフォームに初期値を設定します。
フォームに初期値を設定する時は、useForm
で初期化する際に引数に初期値を渡してあげます。
const { register, handleSubmit } = useForm<Inputs>({
defaultValues: {
name: "taro",
age: 26
}
});
簡単ですね。
ちなみに各input
に直接指定してあげることもできます。
<input {...register("name")} type="text" placeholder="氏名" defaultValue="taro" />
<input {...register("age")} type="number" placeholder="年齢" defaultValue={26} />
React Hook Formの復習はここまでです!お疲れさまでした!
フォームの初期値に非同期なデータを設定する
では前提が揃ったところで本題の「React Hook Formで非同期な初期値を設定する」を考えていきます。
そこで以下のような「2秒後に名前と年齢のデータを返す非同期な関数」の返り値を、フォームの初期値として設定する場合を考えます。
const fetchData = async () => {
await new Promise((resolve) => setTimeout(resolve, 2000));
return { name: "taro", age: 26 };
};
とりあえず愚直にやってみる
まずは愚直にstate
で初期値を持ち、データの取得が完了したらその返り値をsetState
してみます。
const [data, setData] = useState<Inputs>();
const { register, handleSubmit } = useForm<Inputs>({ defaultValues: data });
useEffect(() => {
fetchData().then((data) => setData(data));
}, []);
想像がついていた方もいるかもしれませんが、この方法だとフォームに初期値は反映されません。
useForm
の引数に初期値を渡す場合、useForm
が呼び出された時のdata
(undefined
)が初期値として設定されるためです。
そのため公式ドキュメントにも記されている通り、非同期な初期値を設定する場合はreset
を使うことが多いです。
reset
を使って非同期な初期値を設定する。
reset
はその名の通り再度フォームを初期化するメソッドで、引数にデータを渡すとその値を初期値として初期化してくれます。
ではreset
を使ってデータの取得が終わったら再度フォームを初期化してあげましょう。
const [data, setData] = useState<Inputs>();
const { register, handleSubmit, reset } = useForm<Inputs>();
useEffect(() => {
fetchData().then((data) => setData(data));
}, []);
// データの取得が完了したら再度初期化
useEffect(() => {
reset(data);
}, [reset, data]);
これで晴れて非同期なデータを初期値として反映させることが出来ました!
またuseStateを使わずに書くことも可能です。
const { register, handleSubmit, reset } = useForm<Inputs>();
const resetAsyncForm = useCallback(async () => {
const data = await fetchData();
reset(data);
}, [reset]);
// データの取得が完了したら再度初期化
useEffect(() => {
resetAsyncForm();
}, [resetAsyncForm]);
Component全体はこんな感じになります。
const Form: FC = () => {
const { register, handleSubmit, reset } = useForm<Inputs>();
const [loading, setLoading] = useState<boolean>(true);
const resetAsyncForm = useCallback(async () => {
const data = await fetchData();
reset(data);
setLoading(false);
}, [reset]);
// データの取得が完了したら再度初期化
useEffect(() => {
resetAsyncForm();
}, [resetAsyncForm]);
const onSubmit = (data: Inputs) => console.log(data);
if (loading) return <p>Loading...</p>;
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("name")} type="text" placeholder="氏名" />
<input {...register("age")} type="number" placeholder="年齢" />
<input type="submit" />
</form>
);
};
だいぶ複雑になってきましたね。。。
Custom Hooks等を使えば、もう少しシンプルになるかもしれませんが、やはりreset
で再初期化するという方法を変える必要がありそうです。
またこのreset
による再初期化の方法は、React Hook Formを知らない人からすると一見何をやっているのかがわかりづらいも、個人的には気になるポイントです。
ちなみにReact Hook Formの開発者のBillさんもこの方法に対して、“honestly, this is probably not a pretty and clean approach...”とコメントしています。
そんな悩みを解決するために、今回試してみたのがSuspenseです!
React Hook FormでSuspenseを使う
ではSuspenseを使っていきましょう!
Suspenseを使うと、ComponentにLoading状態を持たせるのではなく、Component自体をLoading状態にすることができます。
そのためComponent内部ではLoading中かどうかを意識する必要がなくなり、非同期なデータの取得を副作用として扱わなくてよくなります。
そもそもreset
で再度初期化をしていた理由が useForm
で初期化するタイミングでは、データが取得されていなかったことだったのを思い出すと、、、
何となく上手く出来そうな気がしますよね…!
では試してみましょう!
Suspenseを使って非同期な初期値を設定する
Suspenseを使うために初期値を受け取るグローバルな変数を定義します。
(stateで扱うこともできますが、少し煩雑になるのとこちらの記事でuhyoさんが述べている理由からグローバル変数を使っています)
let data: Inputs | undefined;
次にSuspenseのお作法に沿ってPromise
をthrow
します!
またPromise
が成功したらその返り値を初期値に代入しています。
if (data === undefined) {
throw fetchData().then((d) => (data = d));
}
そしてフォームのComponentをSuspenseで挟んであげて、fallback
にLoading時の表示を渡します。
<Suspense fallback={<p>Loading...</p>}>
<Form />
</Suspense>
最後にフォームのComponentから不要になった処理を消してあげると、以下のようになります。
const Form: FC = () => {
if (data === undefined) {
throw fetchData().then((d) => (data = d));
}
const { register, handleSubmit } = useForm<Inputs>({ defaultValues: data });
const onSubmit = (data: Inputs) => console.log(data);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("name")} type="text" placeholder="氏名" />
<input {...register("age")} type="number" placeholder="年齢" />
<input type="submit" />
</form>
);
};
reset
やuseEffect
がなくなり、だいぶ見通しが良くなりました!
さらにデータの取得をCutom Hooksに切り出してあげるとよりシンプルになります。
// データの取得をCustomHooksに切り出す
const useData = () => {
if (data === undefined) {
throw fetchData().then((d) => (data = d));
}
return data;
};
const Form: FC = () => {
const { register, handleSubmit } = useForm<Inputs>({
defaultValues: useData()
});
const onSubmit = (data: Inputs) => console.log(data);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("name")} type="text" placeholder="氏名" />
<input {...register("age")} type="number" placeholder="年齢" />
<input type="submit" />
</form>
);
};
これならReact Hook Formを知らない人が見ても、何をしているのかある程度推測できるかなーと思うので、今回はここまでで完成とします!
実際に作ったフォームはCodeSandboxにおいておくので、よかったら触ってみてください。
おまけ
ちょっとだけおまけです。
swrを使った例
swrにはまだ実験的な機能ですが、Suspense Modeがあるので簡単にSuspenseに対応させることが可能です。
const Form: FC = () => {
const { data } = useSWR("data", fetchData, { suspense: true });
const { register, handleSubmit } = useForm<Inputs>({ defaultValues: data });
const onSubmit = (data: Inputs) => console.log(data);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("name")} type="text" placeholder="氏名" />
<input {...register("age")} type="number" placeholder="年齢" />
<input type="submit" />
</form>
);
};
ただSuspense Modeでもdata
の型にundefined
が含まれてしまうなどまだ未完成のところがあり、実務で使うのはまだちょっと早いかなーという所感です。
Suspense Modeでの型のサポートについては、こちらのissueで議論されています。
まとめ
今回はReact Hook Formで非同期な初期値を設定するためにReact18で登場したSuspenseを使ってみました!
個人的にフォームはユーザーが頻繁に操作するUIなため、React18で登場した様々な機能が活きてくるんじゃないかなーとわくわくしています!React Hook Formの今後の進化も楽しみですね!
また普段はZennに記事を投稿したり、Twitterで技術のことをつぶやいているので、よかったらフォローしたり絡んだりしてもらえたらとっても嬉しいです!
記事の内容に関する感想やご指摘、質問等もあればぜひぜひお待ちしてますー!
以上、最後まで読んでいただきありがとうございました!
お世話になったページ
Suspense
React Hook Form