【React19】Actionたちにどうやって型をつけるのか?試してみる。
こんにちは。
React19での新概念「Action」に強く紐づいた新しい機能がたくさん追加されましたね。
つよつよエンジニアたちが日々投稿してくれる記事を読みながら理解を進める毎日の筆者です🙇
本記事で登場するhookたちについての解説は割愛しますので、詳しく知りたい方は公式のドキュメントに加えて、uhyoさんのzenn本を強くお勧めします。(無料か...。いいのか...。すごいな...。)
本記事のモチベ
そんな記事を読んでいくにつれて、実際それらのhooksを使用する際「どんな感じで型つけるんだ?」と疑問に思ったので、useActionState
、useOptimistic
について試してみたことや気になったことをまとめてみたいと思います。
複雑なことは特に何もないのですが、こういう時どうするべきなんだ...?と解決しなかったことも残したいと思います。
本題: どうやって型をつける?
それぞれの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];
useOptimistic
もuseActionState
と同じようなイメージですが、更新用関数(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で実装する前に少しでもイメージが膨らめば幸いです〜🦔
Discussion