📢

同期処理か非同期処理かを意識せずイベントハンドラーを実行する

に公開

以前Reactコンポーネントのイベントハンドラーに非同期処理を渡してもいいという気づきを記事にしました.そのあと,カスタムコンポーネントに対して同様の処理を行いたくなったため,どのように実装するかを調べました.

動機

Webアプリにフォームを準備し,その中で"Submit"ボタンが押されるとバックエンドのAPIと通信を行うようにしました.ここでフォームをカスタムコンポーネントMyFormとして切り出し,Submitボタン押下のイベントをhandleEventというコンポーネントのPropsとして外部から設定するようにしました.

今回はhandleEventに渡される関数は非同期処理であることが分かっているので,Promise<void>を返す関数を型定義しておき,awaitを使って実行します.

ところで,Submitボタンを押下したときのイベントhandleEventが非同期処理じゃない場合はどうすればいいでしょうか.APIサーバーと通信せずインメモリでデータを管理する場合などでは,同期処理で実行することができます.コンポーネントの再利用性を考えると,同期処理も実行できるようにするのがよさそうです.

しかしPropsの型宣言ではhandleEventがプロミスを返す関数しか考慮していないため,同期処理の関数をPropsとして渡すことができません.またhandleEventがプロミスでなければawaitは使えないため,ほかの手段を用いて関数を実行する必要があります.

type CustomComponentProps = {
  handleEvent: () => Promise<void>; // プロミス以外は? 
};

function MyForm({ handleEvent }: CustomComponentProps): React.JSX.Element {
  return (
    <form onSubmit={async (e: React.FormEvent<HTMLFormElement>) => {
      e.preventDefault(); // ページの再読み込みを防止

      await handleEvent();  // プロミス以外は?
    }}>
      <input type="submit">Submit</input>
    </form>
  );
}

方法

結論からいうと以下のような実装をします.

type CustomComponentProps = {
+  handleEvent: () => void;
-  handleEvent: () => Promise<void>; // プロミス以外は? 
};

function MyForm({ handleEvent }: CustomComponentProps): React.JSX.Element {
  return (
    <form onSubmit={async (e: React.FormEvent<HTMLFormElement>) => {
      e.preventDefault(); // ページの再読み込みを防止

+      await Promise.resolve(handleEvent());
-      await handleEvent();  // プロミス以外は?
    }}>
      <input type="submit">Submit</input>
    </form>
  );
}

handleEventの型には戻り値にvoidを返す関数を設定します.
またhandleEventPromise.resolve()に渡した時の戻り値であるプロミスに対してawaitで実行します.

解説

() => void

handleEventの型定義で,空の引数をとり何も値を返さない関数を表します.この「何も値を返さない」というのがミソで,より正確には「関数の戻り値を考慮しない」という宣言です.つまり戻り値を使うことがないのであれば,任意の値を返す関数を割り当てることができます.

type VoidFunc = () => void;
// 以下すべてOK
const func1: VoidFunc = () => {};
const func2: VoidFunc = () => { return 0; };
const func3: VoidFunc = async () => {};

https://zenn.dev/dozo13189/articles/c7d7932c5655a0

つまりvoidを戻り値にする関数をPropsに型定義することで,同期処理の通常の関数でも非同期処理の関数(プロミス)でも渡すことが可能になります.

await Promise.resolve(handleEvent())

プロミスの静的メソッドであるresolve()は引数にとる値にしたがって以下の値を返します.

  • 引数がプロミスの場合は,引数のプロミスそのもの.
  • 引数がそれ以外の場合は,引数の値が受け渡される新たなプロミス.

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise/resolve

ここで引数にhandleEvent()が渡された時は以下の動作になります.

handleEventが非同期関数の場合

  1. handleEvent()を即時評価し,Promise<void>を得る.
  2. Promise.resolve()の引数にPromise<void>が渡されると,引数がプロミスなのでそのまま引数の値を実行結果として即時に返す.
  3. awaitによりPromise<void>が非同期処理で実行される.

handleEventが同期関数の場合

  1. handleEvent()を即時評価し,void型の戻り値なので何らかの値を返す(値の詳細は考慮しない).
  2. Promise.resolve()の引数にhandleEvent()の戻り値が渡されると,引数がプロミスでないので何らかの値が受け渡される新たなPromise<void>を即時に返す.
  3. awaitによりPromise<void>が非同期処理で実行される(しかし空処理なので実質何もしない).

イベントハンドラーの即時評価結果をPromise.resolve()の引数に渡すことで,handleEventが同期関数か非同期関数かを気にせず処理を実行することができます.

まとめ

voidやプロミスをうまく活用することで,イベントハンドラーが同期関数か非同期関数かを意識せずコンポーネント内で実行することができました.

voidやプロミスのルールは初めて知ったときはさらっと流した程度の理解度でしたが,うまく利用すると複雑なこともできるのが面白いなと思いました.TypeScriptは型システムあたりでもこのようなルールを組み合わせて応用するテクニックがあったりして,パズルみたいな印象があります.

Discussion