🛩️

React19から使えるuseActionStateはuseReducerの非同期版

2024/06/15に公開

この記事のまとめ

useActionStateは<form>と共に利用する例が紹介されています。
もちろんその用途でも使えますが、useActionStateは非同期関数を扱えるuseReducerに似たフックで汎用hooksです。

対象読者

  • react19のuseActionStateについて知りたい
  • ある程度reactを知っている
  • Next.jsをある程度知っている

useActionStateとは

公式では<form>とともに使う例が示されており、submitボタンを押したときの処理を記述するhooksのように見えます。

import { useActionState } from "react";

async function increment(previousState, formData) {
  return previousState + 1;
}

function StatefulForm({}) {
  const [state, formAction] = useActionState(increment, 0);
  return (
    <form>
      {state}
      <button formAction={formAction}>Increment</button>
    </form>
  )
}

しかし、useActionStateの引数や戻り値の型をよく見てください。見覚えありませんか?

import { useReducer } from 'react';

function reducer(state, action) {
  // ...
}

function MyComponent() {
  const [state, dispatch] = useReducer(reducer, { age: 42 });
  // ...

useReducerはreducerという関数と初期値を受け取ります。
reducerは現在の状態とpayloadを受け取り、新しい状態を返します。

useActionStateと似ています。どちらも現在の状態とpayloadを受け取り新しい状態を返す関数と、初期値を引数に受け取ります。

useReducerとuseActionStateの違い

大きな違いが1つあります。
useActionStateは非同期関数を使えます。そして、非同期関数が実行中か判定する変数を返却します。

const action = async (oldState: State, payload: unknown): State => {
    // oldStateとpayloadから新しい状態を計算
    // async関数なのでfetchなどをawaitできる
    return newState;
}

const [state, dispatch, isPending] = useActionState(action, initialState);

非同期関数を使えるということで、Next.jsで利用できるserver actionsも使えます。

actionは非同期関数を登録できますが、dispatchは同期関数です。
どういう挙動になるか見てみました。

dispatchが同期関数なのにactionが非同期関数で動くわけ

dispatchはactionをスケジューリングする関数であり、dispatch関数内でactionを実行しているわけではありません。ここがuseReducerと異なる点です。
useReducerはdispatch関数の内部でactionを実行しているので、actoinに重たい処理を書くことはご法度でした。

useActionStateのdispatchはスケジューリングだけなので、呼び出してもユーザーの操作をブロックしません。

useActionStateは裏側でuseTransitionを使っています。
なので、制御を入れないと前のaction実行中にdispatchを何度も呼ぶことができます。

dispatchを何度も読んだらどうなるのでしょうか
actionが直列にスケジューリングされます。
ちゃんと順番通りに実行してくれるので、結果が壊れることはありません。
ただし、非同期関数が何個もスケジューリングさせる必要ないと思うので、isPendingでボタンを押せなくする等の制御した方が良いです。

formのactionになぜ使っているのか

formのactionは別に前の状態を参照する必要ないですし、新しい状態を返す必要もないです。

import { useActionState } from 'react';
import { action } from './actions.js';

function MyComponent() {
  const [state, formAction, isPending] = useActionState(action, null);
  // ...
  return (
    <form action={formAction}>
      {/* ... */}
    </form>
  );
}

formの内容を登録するserver actionをactionとして、初期値・戻り値ともにnullにすることで、formのactionを管理するhooksとしても使えます。この時のpayloadはformDataです。

action実行中にユーザーの操作をブロックせず、actionが実行中かどうか簡単に管理できます。
この使い方もとても便利ですが、useActionStateはform専用ではなく、もっと汎用なhooksです。

最後に

react 19の正式リリースされていないので変わる可能性があります。
また、私の理解が異なっていたらコメント等にて教えてください。

Discussion