Gemcook Tech Blog
🤾

【React19】Actionたちにどうやって型をつけるのか?試してみる。

2024/06/06に公開

こんにちは。
React19での新概念「Action」に強く紐づいた新しい機能がたくさん追加されましたね。
つよつよエンジニアたちが日々投稿してくれる記事を読みながら理解を進める毎日の筆者です🙇

https://ja.react.dev/blog/2024/04/25/react-19

本記事で登場するhookたちについての解説は割愛しますので、詳しく知りたい方は公式のドキュメントに加えて、uhyoさんのzenn本を強くお勧めします。(無料か...。いいのか...。すごいな...。)

https://zenn.dev/uhyo/books/react-19-new

本記事のモチベ

そんな記事を読んでいくにつれて、実際それらのhooksを使用する際「どんな感じで型つけるんだ?」と疑問に思ったので、useActionStateuseOptimisticについて試してみたことや気になったことをまとめてみたいと思います。
複雑なことは特に何もないのですが、こういう時どうするべきなんだ...?と解決しなかったことも残したいと思います。

本題: どうやって型をつける?

それぞれのhookについてどの様に型をつければいいかみていきましょう。

useActionState

useActionStateの定義
export function useActionState<State>(
    action: (state: Awaited<State>) => State | Promise<State>,
    initialState: Awaited<State>,
    permalink?: string,
): [state: Awaited<State>, dispatch: () => void, isPending: boolean];

export function useActionState<State, Payload>(
    action: (state: Awaited<State>, payload: Payload) => State | Promise<State>,
    initialState: Awaited<State>,
    permalink?: string,
): [state: Awaited<State>, dispatch: (payload: Payload) => void, isPending: boolean];

useActionStateの第一引数に渡すcallbackにpayloadをわたすかどうかで分岐していますね。
実際にTypeScriptで型をつけるときも、受け取るdispatch関数にpayloadを受け取りたいかどうか?によって型の付け方が変わってきます。
単純なカウンター用のactionを例に考えてみます。

payloadなし

increment関数でpayloadを使用しない場合、useActionStateの第1 引数にcountの型を指定するか、もしくは何も指定しなくてOKです。(指定しない場合は、第2引数のinitialStateの値から推論してくれます。)

const [count, increment, isPending] = useActionState<number>((curr) => {
    return curr + 1;
}, 0);

// もしくは...。

const [count, increment, isPending] = useActionState((curr) => {
    return curr + 1;
}, 0);

/**
typeof count // number
typeof curr // number
typeof increment // () => void
**/

payloadあり

payloadを使用しない時は、型引数を指定してもしなくてもどっちでもOKでしたが、payloadを受け取りたい時はどうでしょうか。
こちらの場合は、useActionStateの第2 引数に受け取りたいpayloadの型を指定する必要があります。

type CountType = "increment" | "decrement"

const [count, updateCount, isPending] = useActionState<number, CountType>((curr, payload) => {
    switch (payload) {
        case "increment":
            return curr + 1;
        case "decrement":
            return curr - 1;
    }
},0);

/**
typeof count // number
typeof curr // number
typeof increment // (payload: CountType) => void
**/

useOptimistic

useOptimistic の定義
export function useOptimistic<State>(
    passthrough: State,
): [State, (action: State | ((pendingState: State) => State)) => void];

export function useOptimistic<State, Action>(
    passthrough: State,
    reducer: (state: State, action: Action) => State,
): [State, (action: Action) => void];

useOptimisticuseActionStateと同じようなイメージですが、更新用関数(reducer)を使わずに本hookを利用するユースケースが思いつかないので、更新用関数を利用するパターンだけを考えたいと思います。
基本的には推論してくれないので、第1、第2どちらの型引数も指定する必要があります。

const [displayCount, addOptimisticCount] = useOptimistic<number, number>(
    0,
    (curr, optimisticVal) => {
        return curr + optimisticVal;
    }
);

/**
typeof curr // number(第1型引数の方)
typeof optimisticVal // number(第2型引数の方)
typeof addOptimisticCount // (action: number) => void (第2型引数の方)
**/

疑問: こういう時どうする?

さて、上記の様に型をつけてあげれば問題なく実装できることはわかったのですが、いろいろコードを書いていくにつれて筆者はこんなことを思いました...。

「コールバック関数長くなりがちじゃね...🤔?」「切り出したい時ありそうじゃね...🤔?」...と。

切り出す。というのは以下の様な形を想像しています。

const nagamenoAction = (curr, payload) => {
    // ...
    // なんやかんや
    // ...
    return curr + payload
}

const [count, increment, isPending] = useActionState(nagamenoAction, 0);

さて、上記の様な形が良いか悪いか?という議論はさておいていただき、こうしたいとなった時、問題になってくるのは、切り出した関数の型です。

たとえばuseStateの更新用関数をpropsで渡したい際などはDispatch<SetStateAction<string>>の様な形で型指定できると思うのですが、
前述(アコーディオンの中の各hookの定義)の通り、そういった型関数が用意されてはいなさそうです...。

筆者自身全く答えは出ていないのですが、

type Action<S, P> = Parameters<typeof useActionState<S, P>>[0];

の様な型関数を作成してしまうのがいいのかなと思っています。

type CountType = "increment" | "decrement";

type Action<S, P> = Parameters<typeof useActionState<S, P>>[0];

const nagamenoAction: Action<number, CountType> = (curr, payload) => {
  switch (payload) {
    case "increment":
      return curr + 1;
    case "decrement":
      return curr - 1;
  }
};

const [count, updateCount, isPending] = useActionState<number, CountType>(
  nagamenoAction,
  0
);

んーーー....🤔。
より良い方法がありましたら、ぜひコメントしていただきたいです!!!

まとめ

さて、
というわけで今回は「型を付けつつ新hooksたちを使うには。」についてまとめてみました。
実際にRect19で実装する前に少しでもイメージが膨らめば幸いです〜🦔

Gemcook Tech Blog
Gemcook Tech Blog

Discussion