React 19の「アクション」による非同期データ更新の簡略化
ここまで、React 19のトランジションで「非同期なデータ取得」処理を簡潔に記述できることを確認しました。ここからはReact 19によって「非同期なデータ更新」の実装がどう変わるかにフォーカスします。
まず、React 19で新たに導入された「アクション」という概念を紹介します。アクションは非同期処理をより簡単に管理できる仕組みです。公式情報では「非同期トランジションを使用する関数を規約として『アクション』と呼ぶ」と説明されています。
従来のReactでは、データの送信や非同期処理の状態管理は各コンポーネントの内部で個別にハンドリングする必要があり、複雑なコードになることもありました。今後はアクションを用いることで、非同期処理における状態やエラーハンドリング、成功・失敗のフィードバック表示などを簡潔に管理できます。
[コラム]「アクション」と「非同期関数」は同じか
端的には「アクション=非同期関数」という理解でもよさそうですが、「非同期トランジションを使用する関数」と表現されていることがミソです。つまり、「トランジション」の特徴を受け継いだ非同期関数が「アクション」だということになります。
具体的には、トランジションで提供されていた「送信中状態の管理」と「応答性の高いレンダリング」という特徴を非同期関数でも利用できるようにしたものと言えます。これに加え、フォームから直接的に呼び出しやすくなり、「楽観的更新」を使ったユーザー体験の向上が可能になっています。
まず、従来のReactでは、データの更新処理をどのように書いていたかを見てみます。ここでは「タイトル」を更新する簡単なフォームを考えます。
function UpdateTitle({ value, onUpdate }) { const [error, setError] = useState(null); // エラー情報 const [isPending, setIsPending] = useState(false); // 処理中フラグ const handleSubmit = async (event) => { event.preventDefault(); // 処理中フラグを立てる setIsPending(true); // フォームの入力値で更新処理を実行 const title = event.target.title.value; const { data, error } = await updateTitle(title); // 処理中フラグを倒す setIsPending(false); if (error) { // エラーの場合はエラー情報を表示 setError(error); return; } onUpdate(data); }; return ( <form onSubmit={handleSubmit}> <input name="title" defaultValue={value} disabled={isPending} /> <button type="submit" disabled={isPending}> 更新 </button> {error && <p>{error}</p>} <div>value: {value}</div> </form> ); }
なお、このUpdateTitle
コンポーネントはリスト4のように親コンポーネントから呼び出されているものとします。
function App() { const [title, setTitle] = useState("React はじめました"); return <UpdateTitle value={title} onUpdate={setTitle} />; }
また、ここでupdateTitle
関数はリスト5のようになんらかの更新処理を行って、title
(エラーの場合はerror
)を返す処理だとします。ダミーのためsetTimeout
で1秒間のウェイトを入れています。
function updateTitle(value) { return new Promise((resolve) => // なんらかの更新処理 setTimeout(() => { resolve({ data: value, error: null }); // 成功時 // resolve({ error: "更新に失敗しました" }); // 失敗時 }, 1000) ); }
以後、このUpdateTitle
コンポーネントをベースに変更を加えていきます。
useActionStateフックと<form>
アクション:非同期のフォーム送信処理をシンプルに
useActionState
は、「アクション」の結果に基づいて状態管理を行うために、React 19で導入された新しいフックです。React 19の新APIの中でも特に幅広い用途を持っており重要です。
またReact 19では<form>
に「アクション」を直接指定できるようになり、より簡潔に「フォームによる送信」を制御できるようになりました。Reactの公式ドキュメントでもuseActionState
は<form>
アクションと組み合わせて使用することが前提になっています。
早速、UpdateTitle
コンポーネントをuseActionState
と<form>
アクションを使って書き換えてみます。
function UpdateTitle({ value, onUpdate }) { // useActionStateの戻り値はステート、アクション、送信中状態フラグ ... (1) const [error, submitAction, isPending] = useActionState( // 非同期関数をアクション本体として渡す ... (2) async (previousState, formData) => { // 第1引数: 変更前のステート, 第2引数: `FormData` ... (3) const value = formData.get("title"); const { data, error } = await updateTitle(value); if (!error) { onUpdate(data); } return error; }, null // ステートの初期値 ); return ( // formのactionにアクションを直接指定できる ... (4) <form action={submitAction}> <input name="title" defaultValue={value} disabled={isPending} /> <button type="submit" disabled={isPending}> 更新 </button> {error && <p>{error}</p>} <div>value: {value}</div> </form> ); }
動作自体はuseTransition
の場合と変わっていませんが、注目すべきは以下の点です。
(1)useActionState
の戻り値はstate、アクション、送信中状態フラグ
useState
はもはや一つもなくなり、新しいuseActionState
フックだけになりました。これまで扱ってきたerror
という「ステート」と、「送信中状態」(isPending
)のいずれも、useActionState
フックから取得しています。
ここで、useActionState
の2番目の戻り値が「アクション」であることを意識しましょう。このアクションは前述の通り「非同期トランジション」の性質をもっているので、内部で実行されるステート更新は「優先度の低い更新」として扱われます。
なお、ここでは「ステート」としてエラー文字列を管理していますが、ここで管理するステートはsetState
と同様にオブジェクトなどの値でも問題ありません。
(2)useActionState
にはアクションの本体となる非同期関数を渡す
useActionState
の第1引数は、処理を実行する非同期関数です。
ここで「非同期関数」を渡していることが特徴です。useTransition
の場合と同様に非同期関数を渡すことで、Promiseが解決するまでisPending
がtrue
に保たれるため、「送信中」であることを簡潔に管理できます。
第2引数にはuseState
と同様に、state の初期値を指定します。
(3)アクションは第1引数に変更前のステート、第2引数にFormData
を受け取る
アクションとして指定する非同期関数の引数が「変更前のステート」と「FormData」になっていることに注目しましょう。
「変更前のステート」が得られるのはsetState
と似ています。ステートの値を前の値に基づいて変更する場合のために、「前のステート」へアクセスする手段が提供されています。
次に第2引数は<form>
アクションから渡されるもので、共通Web APIのFormData
インターフェースになっています。よって、フォームのデータに対してFormData.get("属性名")
でアクセスできます。DOM要素の値を参照する必要はなくなりました。
[Note]「前のステート」へのアクセス
「前のステート」にアクセスする必要があるのは、更新後の値が更新前の値に依存している場合です。典型的なユースケースとしてユーザーが「いいね」を押すたびにカウントを増やすような場合が挙げられます。
const [count, likeAction, isPending] = useActionState( async (prevCount) => { await incrementLike(value); return prevCount + 1; } );
あるいはステートがオブジェクトや配列の場合に、一部のプロパティや要素だけを更新する場合も同様です。
const [user, updateUserNameAction, isPending] = useActionState( async (prevUser, formData) => { const name = formData.get("name"); await updateUser(name); return { ...prevUser, name }; } );
(4)<form>
のaction
にアクションを直接渡している
useActionState
フックの戻り値の「アクション」をそのまま<form>
タグのaction
属性に直接渡しています。これが「<form>
アクション」の機能です。
前述のとおり、<form>
アクションを利用すると、FormData
インターフェースのオブジェクトが関数の引数として受け取れます。
まとめ
今回は、React 19で導入されたuseActionState
と<form>
アクションによって、非同期データ更新処理の実装が大幅に簡略化されました。これにより、開発効率が向上すると同時に、コードの可読性も改善される点が大きな魅力です。
次回はReact 19の「アクション」関連の付加的なAPIのほか、その他の新機能や変更点について紹介していきますので、ぜひご期待ください。