React 18のSuspenseを使ってReact Hook Formの非同期な初期値の扱いを簡単にする

# はじめに
普段はスタートアップで建設業界向けのSaaSの開発をしている[taro](https://twitter.com/taroro_tarotaro)と申します。
今回はReact18で登場した[Suspense](https://reactjs.org/docs/react-api.html#reactsuspense)を触っていたら、[React Hook Form](https://react-hook-form.com/)でフォームの初期値に非同期な値を設定するのが簡単になりそうだなーって思ったので、試してみました。
React Hook Formを使ったことがない方でもきっとわかるような内容になっていると思うので、ぜひぜひ読んでみてくださいー！

この記事はこちらのイベントに参加しています。

https://qiita.com/official-events/6677e0827fec55a3b95f

# 前提を揃えるためにReact Hook Formを少し復習
本題に入る前に、React Hook Formについて少し復習して前提を揃えていこうと思います。
（「復習はいらないよー！」って方は、[React Hook FormでSuspenseを使う](https://qiita.com/taro28/items/df91fc26b3ba461d9d5e#react-hook-form%E3%81%A7suspense%E3%82%92%E4%BD%BF%E3%81%86)まで飛んでください！）

またSuspenseについては、公式ドキュメントや別の記事等をご参考ください。
特に[uhyo](https://twitter.com/uhyo_)さんの記事がとてもわかりやすくまとまっているのでおすすめです！

https://qiita.com/uhyo/items/bbc22022fe846fd2b763

https://zenn.dev/uhyo/books/react-concurrent-handson

## React Hook Formとは？
React Hook FormはHooksベースでFormを管理できるのが特徴で、Reactのフォーム管理だと一位を争うほどよく採用されているライブラリです。

https://npmtrends.com/final-form-vs-formik-vs-react-final-form-vs-react-hook-form-vs-redux-form

### 簡単なフォームを作ってみる
今回はReact Hook Formを使って、名前と年齢を入力してコンソールに出力する簡単なフォームを作ってみます。
![画面収録-2022-07-17-6.08.30.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/374916/351c4a8b-96f9-0c15-bca1-be62ce936c8a.gif)

フォームのComponentはこちらです。
```tsx
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`を呼び出してフォームを初期化し、フォームを管理するためのメソッドを受け取る**
```tsx
const { register, handleSubmit } = useForm<Inputs>();
```
https://react-hook-form.com/api/useform

**`register`で各`input`をReact Hook Formの管理下に登録する**
React Hook Formでは`name`属性(今回は`name`と`age`)を使って、各`input`を識別して管理します。
```tsx
<input {...register("name")} type="text" placeholder="氏名" />
<input {...register("age")} type="number" placeholder="年齢" />
```
https://react-hook-form.com/api/useform/register

**`handleSubmit`でReact Hook Formで管理しているフォームのデータを受け取る**
`handleSubmit`でラップした関数(`onSubmit`)には、Validateされたフォームのデータが渡されます。
```tsx
  const onSubmit = (data: Inputs) => console.log(data);

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
```
https://react-hook-form.com/api/useform/handlesubmit

### フォームに初期値を設定する
次にフォームに初期値を設定します。
フォームに初期値を設定する時は、`useForm`で初期化する際に引数に初期値を渡してあげます。
```tsx
const { register, handleSubmit } = useForm<Inputs>({
  defaultValues: {
    name: "taro",
    age: 26
  }
});
```
簡単ですね。

ちなみに各`input`に直接指定してあげることもできます。
```tsx
<input {...register("name")} type="text" placeholder="氏名" defaultValue="taro" />
<input {...register("age")} type="number" placeholder="年齢" defaultValue={26} />
```

---
React Hook Formの復習はここまでです！お疲れさまでした！

# フォームの初期値に非同期なデータを設定する
では前提が揃ったところで本題の「React Hook Formで非同期な初期値を設定する」を考えていきます。
そこで以下のような「2秒後に名前と年齢のデータを返す非同期な関数」の返り値を、フォームの初期値として設定する場合を考えます。
```tsx
const fetchData = async () => {
  await new Promise((resolve) => setTimeout(resolve, 2000));
  return { name: "taro", age: 26 };
};
```

## とりあえず愚直にやってみる
まずは愚直に`state`で初期値を持ち、データの取得が完了したらその返り値を`setState`してみます。
```tsx
const [data, setData] = useState<Inputs>();
const { register, handleSubmit } = useForm<Inputs>({ defaultValues: data });

useEffect(() => {
  fetchData().then((data) => setData(data));
}, []);
```
想像がついていた方もいるかもしれませんが、この方法だとフォームに初期値は反映されません。
`useForm`の引数に初期値を渡す場合、`useForm`が呼び出された時の`data`(`undefined`)が初期値として設定されるためです。
そのため公式ドキュメントにも記されている通り、非同期な初期値を設定する場合は`reset`を使うことが多いです。

https://react-hook-form.com/api/useform/reset

### `reset`を使って非同期な初期値を設定する。
`reset`はその名の通り再度フォームを初期化するメソッドで、引数にデータを渡すとその値を初期値として初期化してくれます。
では`reset`を使ってデータの取得が終わったら再度フォームを初期化してあげましょう。
```tsx
const [data, setData] = useState<Inputs>();
const { register, handleSubmit, reset } = useForm<Inputs>();

useEffect(() => {
  fetchData().then((data) => setData(data));
}, []);

// データの取得が完了したら再度初期化
useEffect(() => {
  reset(data);
}, [reset, data]);
```
これで晴れて非同期なデータを初期値として反映させることが出来ました！
またuseStateを使わずに書くことも可能です。
```tsx
const { register, handleSubmit, reset } = useForm<Inputs>();

const resetAsyncForm = useCallback(async () => {
  const data = await fetchData();
  reset(data);
}, [reset]);

// データの取得が完了したら再度初期化
useEffect(() => {
  resetAsyncForm();
}, [resetAsyncForm]);
```

さらに次のようにLoadingを表示させると、
![画面収録-2022-07-17-7.08.54.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/374916/f0eb6038-cd58-b699-5064-8207d65f254b.gif)

Component全体はこんな感じになります。
```tsx
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...*”とコメントしています。

https://github.com/react-hook-form/react-hook-form/issues/2492

そんな悩みを解決するために、今回試してみたのがSuspenseです！


# React Hook FormでSuspenseを使う
ではSuspenseを使っていきましょう！
Suspenseを使うと、**ComponentにLoading状態を持たせる**のではなく、**Component自体をLoading状態**にすることができます。
そのためComponent内部ではLoading中かどうかを意識する必要がなくなり、非同期なデータの取得を副作用として扱わなくてよくなります。

そもそも`reset`で再度初期化をしていた理由が **`useForm`で初期化するタイミングでは、データが取得されていなかったこと**だったのを思い出すと、、、
何となく上手く出来そうな気がしますよね…！

では試してみましょう！

## Suspenseを使って非同期な初期値を設定する
Suspenseを使うために初期値を受け取るグローバルな変数を定義します。
（stateで扱うこともできますが、少し煩雑になるのと[こちら](https://zenn.dev/uhyo/books/react-concurrent-handson/viewer/data-fetching-1#usestate%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E3%81%BF%E3%82%8B%EF%BC%88%E5%A4%B1%E6%95%97%E4%BE%8B%EF%BC%89)の記事でuhyoさんが述べている理由からグローバル変数を使っています）
```tsx
let data: Inputs | undefined;
```

次にSuspenseのお作法に沿って`Promise`を`throw`します！
また`Promise`が成功したらその返り値を初期値に代入しています。
```tsx
if (data === undefined) {
  throw fetchData().then((d) => (data = d));
}
```

そしてフォームのComponentをSuspenseで挟んであげて、`fallback`にLoading時の表示を渡します。
```tsx
<Suspense fallback={<p>Loading...</p>}>
  <Form />
</Suspense>
```

最後にフォームのComponentから不要になった処理を消してあげると、以下のようになります。
```tsx
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に切り出してあげるとよりシンプルになります。
```tsx
// データの取得を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においておくので、よかったら触ってみてください。

https://codesandbox.io/embed/gracious-diffie-onk3wt?fontsize=14&hidenavigation=1&theme=dark&view=preview

# おまけ
ちょっとだけおまけです。

## swrを使った例
swrにはまだ実験的な機能ですが、Suspense Modeがあるので簡単にSuspenseに対応させることが可能です。
```tsx
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で議論されています。

https://github.com/vercel/swr/issues/1412


# まとめ
今回はReact Hook Formで非同期な初期値を設定するためにReact18で登場したSuspenseを使ってみました！
個人的にフォームはユーザーが頻繁に操作するUIなため、React18で登場した様々な機能が活きてくるんじゃないかなーとわくわくしています！React Hook Formの今後の進化も楽しみですね！
また普段はZennに記事を投稿したり、Twitterで技術のことをつぶやいているので、よかったらフォローしたり絡んだりしてもらえたらとっても嬉しいです！

https://zenn.dev/taroro28

https://twitter.com/taroro_tarotaro

記事の内容に関する感想やご指摘、質問等もあればぜひぜひお待ちしてますー！
以上、最後まで読んでいただきありがとうございました！

# お世話になったページ
## Suspense
https://reactjs.org/docs/react-api.html#reactsuspense

https://qiita.com/uhyo/items/bbc22022fe846fd2b763

https://zenn.dev/uhyo/books/react-concurrent-handson

## React Hook Form
https://react-hook-form.com/