ReactのuseActionStateフックの能力を最大限に引き出しましょう。フォーム管理を簡素化し、保留状態を処理し、実践的で詳細な例を通じてユーザー体験を向上させる方法を学びます。
React useActionState: モダンなフォーム管理のための包括的ガイド
Web開発の世界は絶えず進化しており、Reactエコシステムはその変化の最前線にいます。近年のバージョンで、Reactはインタラクティブで回復力のあるアプリケーションの構築方法を根本的に改善する強力な機能を導入しました。その中で最も影響力のあるものの一つが、フォームや非同期操作の処理方法を大きく変えるuseActionStateフックです。このフックは、以前は実験的なリリースでuseFormStateとして知られていましたが、今では現代のReact開発者にとって安定した不可欠なツールとなっています。
この包括的なガイドでは、useActionStateを深く掘り下げていきます。それが解決する問題、その中心的な仕組み、そしてuseFormStatusのような補完的なフックと共にそれを活用して優れたユーザー体験を創出する方法を探ります。単純なコンタクトフォームを構築している場合でも、複雑でデータ集約的なアプリケーションを構築している場合でも、useActionStateを理解することで、コードはよりクリーンで、より宣言的で、より堅牢になります。
問題点:従来のフォーム状態管理の複雑さ
useActionStateの洗練さを評価する前に、まずそれが対処する課題を理解しなければなりません。長年にわたり、Reactでのフォーム状態の管理は、useStateフックを使用した予測可能だがしばしば面倒なパターンを伴っていました。
一般的なシナリオを考えてみましょう:リストに新しい商品を追加する単純なフォームです。私たちはいくつかの状態を管理する必要があります:
- 商品名の入力値。
- API呼び出し中にユーザーにフィードバックを与えるためのローディングまたは保留状態。
- 送信が失敗した場合にメッセージを表示するためのエラー状態。
- 完了時の成功状態またはメッセージ。
典型的な実装は次のようになります:
例:「古い方法」 - 複数のuseStateフックを使用
// 架空のAPI関数
const addProductAPI = async (productName) => {
await new Promise(resolve => setTimeout(resolve, 1500));
if (!productName || productName.length < 3) {
throw new Error('Product name must be at least 3 characters long.');
}
console.log(`Product "${productName}" added.`);
return { success: true };
};
// コンポーネント
{error}import { useState } from 'react';
function OldProductForm() {
const [productName, setProductName] = useState('');
const [error, setError] = useState(null);
const [isPending, setIsPending] = useState(false);
const handleSubmit = async (event) => {
event.preventDefault();
setIsPending(true);
setError(null);
try {
await addProductAPI(productName);
setProductName(''); // 成功時に入力をクリア
} catch (err) {
setError(err.message);
} finally {
setIsPending(false);
}
};
return (
id="productName"
name="productName"
value={productName}
onChange={(e) => setProductName(e.target.value)}
/>
{isPending ? 'Adding...' : 'Add Product'}
{error &&
);
}
このアプローチは機能しますが、いくつかの欠点があります:
- ボイラープレート:概念的には単一のフォーム送信プロセスであるものを管理するために、3つの別々のuseState呼び出しが必要です。
- 手動の状態管理:開発者は、try...catch...finallyブロック内でローディングとエラーの状態を正しい順序で手動で設定およびリセットする責任があります。これは反復的でエラーが発生しやすいです。
- 密結合:フォーム送信結果を処理するロジックが、コンポーネントのレンダリングロジックと密接に結合しています。
useActionStateの導入:パラダイムシフト
useActionStateは、フォーム送信などの非同期アクションの状態を管理するために特別に設計されたReactフックです。アクション関数の結果に状態を直接接続することで、プロセス全体を合理化します。
そのシグネチャは明確かつ簡潔です:
const [state, formAction] = useActionState(actionFn, initialState);
その構成要素を分解してみましょう:
actionFn(previousState, formData)
: これは作業(例:API呼び出し)を実行する非同期関数です。引数として前の状態とフォームデータを受け取ります。重要なのは、この関数が返すものが新しい状態になることです。initialState
: これは、アクションが初めて実行される前の状態の値です。state
: これは現在の状態です。最初はinitialStateを保持し、実行のたびにactionFnの戻り値に更新されます。formAction
: これは、あなたのアクション関数の新しいラップされたバージョンです。この関数を<form>
要素のaction
プロップに渡すべきです。Reactはこのラップされた関数を使用して、アクションの保留状態を追跡します。
実践例:useActionStateによるリファクタリング
では、私たちの商品フォームをuseActionStateを使ってリファクタリングしてみましょう。改善はすぐに明らかになります。
まず、アクションロジックを適応させる必要があります。エラーをスローする代わりに、アクションは結果を記述する状態オブジェクトを返す必要があります。
例:「新しい方法」 - useActionStateを使用
// useActionStateで動作するように設計されたアクション関数
const addProductAction = async (previousState, formData) => {
const productName = formData.get('productName');
await new Promise(resolve => setTimeout(resolve, 1500)); // ネットワーク遅延をシミュレート
if (!productName || productName.length < 3) {
return { message: 'Product name must be at least 3 characters long.', success: false };
}
console.log(`Product "${productName}" added.`);
// 成功時、成功メッセージを返します。
return { message: `Successfully added "${productName}"`, success: true };
};
// リファクタリングされたコンポーネント
{state.message} {state.message}import { useActionState } from 'react';
// 注:次のセクションで保留状態を処理するためにuseFormStatusを追加します。
function NewProductForm() {
const initialState = { message: null, success: false };
const [state, formAction] = useActionState(addProductAction, initialState);
return (
{!state.success && state.message && (
)}
{state.success && state.message && (
)}
);
}
これがどれほどクリーンになったか見てください!3つのuseStateフックを単一のuseActionStateフックに置き換えました。コンポーネントの責任は、今や純粋に`state`オブジェクトに基づいてUIをレンダリングすることです。すべてのビジネスロジックは`addProductAction`関数内にきれいにカプセル化されています。状態はアクションが返すものに基づいて自動的に更新されます。
しかし待ってください、保留状態についてはどうでしょうか?フォームが送信中の間、ボタンを無効にするにはどうすればよいでしょうか?
useFormStatusによる保留状態の処理
Reactは、この問題を解決するために設計されたコンパニオンフック、useFormStatusを提供しています。これは最後のフォーム送信のステータス情報を提供しますが、重要なルールがあります:追跡したいステータスを持つ<form>
の内部でレンダリングされるコンポーネントから呼び出されなければなりません。
これは関心の分離を促進します。送信ボタンのように、フォームの送信ステータスを認識する必要があるUI要素専用のコンポーネントを作成します。
useFormStatusフックは、いくつかのプロパティを持つオブジェクトを返します。最も重要なのは`pending`です。
const { pending, data, method, action } = useFormStatus();
pending
: 親フォームが現在送信中の場合は`true`、そうでない場合は`false`のブール値。data
: 送信されているデータを含む`FormData`オブジェクト。method
: HTTPメソッド(`'get'`または`'post'`)を示す文字列。action
: フォームの`action`プロップに渡された関数への参照。
ステータスを認識する送信ボタンの作成
専用の`SubmitButton`コンポーネントを作成し、私たちのフォームに統合しましょう。
例:SubmitButtonコンポーネント
import { useFormStatus } from 'react-dom';
// 注:useFormStatusは'react'ではなく'react-dom'からインポートします。
function SubmitButton() {
const { pending } = useFormStatus();
return (
{pending ? 'Adding...' : 'Add Product'}
);
}
では、メインのフォームコンポーネントを更新して、それを使用するようにしましょう。
例:useActionStateとuseFormStatusを使用した完全なフォーム
{state.message} {state.message}import { useActionState } from 'react';
import { useFormStatus } from 'react-dom';
// ... (addProductAction関数は同じまま)
function SubmitButton() { /* ... 上記で定義した通り ... */ }
function CompleteProductForm() {
const initialState = { message: null, success: false };
const [state, formAction] = useActionState(addProductAction, initialState);
return (
{/* 成功時にinputをリセットするためにkeyを追加できます */}
{!state.success && state.message && (
)}
{state.success && state.message && (
)}
);
}
この構造により、`CompleteProductForm`コンポーネントは保留状態について何も知る必要がありません。`SubmitButton`は完全に自己完結しています。この構成的なパターンは、複雑で保守性の高いUIを構築する上で非常に強力です。
プログレッシブエンハンスメントの力
この新しいアクションベースのアプローチの最も深遠な利点の一つは、特にサーバーアクションと共に使用した場合、自動的なプログレッシブエンハンスメントです。これは、ネットワーク状況が不安定であったり、ユーザーが古いデバイスや無効化されたJavaScriptを使用している可能性があるグローバルなオーディエンス向けのアプリケーションを構築する上で、非常に重要な概念です。
仕組みは次のとおりです:
- JavaScriptなしの場合:ユーザーのブラウザがクライアントサイドのJavaScriptを実行しない場合、
<form action={...}>
は標準的なHTMLフォームとして機能します。サーバーへのフルページリクエストを行います。Next.jsのようなフレームワークを使用している場合、サーバーサイドのアクションが実行され、フレームワークは新しい状態でページ全体を再レンダリングします(例:検証エラーの表示)。アプリケーションは、SPAのような滑らかさはないものの、完全に機能します。 - JavaScriptありの場合:JavaScriptバンドルがロードされ、Reactがページをハイドレートすると、同じ`formAction`がクライアントサイドで実行されます。フルページの再読み込みの代わりに、通常のfetchリクエストのように動作します。アクションが呼び出され、状態が更新され、コンポーネントの必要な部分のみが再レンダリングされます。
これは、フォームロジックを一度書くだけで、両方のシナリオでシームレスに機能することを意味します。デフォルトで回復力があり、アクセスしやすいアプリケーションを構築できることは、世界中のユーザー体験にとって大きな勝利です。
高度なパターンとユースケース
1. サーバーアクション vs. クライアントアクション
useActionStateに渡す`actionFn`は、標準的なクライアントサイドの非同期関数(私たちの例のように)でも、サーバーアクションでもかまいません。サーバーアクションは、クライアントコンポーネントから直接呼び出すことができるサーバー上で定義された関数です。Next.jsのようなフレームワークでは、関数本体の先頭に"use server";
ディレクティブを追加することで定義します。
- クライアントアクション:クライアントサイドの状態にのみ影響を与える、またはクライアントから直接サードパーティAPIを呼び出すミューテーションに最適です。
- サーバーアクション:データベースや他のサーバーサイドリソースを伴うミューテーションに最適です。各ミューテーションのために手動でAPIエンドポイントを作成する必要がなくなるため、アーキテクチャが簡素化されます。
素晴らしいのは、useActionStateが両方で全く同じように動作することです。コンポーネントのコードを変更することなく、クライアントアクションをサーバーアクションに置き換えることができます。
2. `useOptimistic`によるオプティミスティックアップデート
さらに応答性の高い感覚を得るために、useActionStateをuseOptimisticフックと組み合わせることができます。オプティミスティックアップデートとは、非同期アクションが成功すると*仮定して*、UIを即座に更新することです。もし失敗した場合は、UIを前の状態に戻します。
コメントを追加するソーシャルメディアアプリを想像してみてください。オプティミスティックに、リクエストがサーバーに送信されている間に新しいコメントをリストに即座に表示します。useOptimisticは、このパターンを簡単に実装するためにアクションと連携するように設計されています。
3. 成功時のフォームのリセット
よくある要件は、送信が成功した後にフォームの入力をクリアすることです。useActionStateでこれを達成するにはいくつかの方法があります。
- Keyプロップのトリック:`CompleteProductForm`の例で示したように、inputまたはフォーム全体に一意の`key`を割り当てることができます。keyが変更されると、Reactは古いコンポーネントをアンマウントし、新しいコンポーネントをマウントするため、その状態が効果的にリセットされます。keyを成功フラグに結びつける(`key={state.success ? 'success' : 'initial'}`)のは、シンプルで効果的な方法です。
- 制御されたコンポーネント:必要であれば、引き続き制御されたコンポーネントを使用できます。useStateで入力の値を管理することで、useActionStateからの成功状態を監視するuseEffect内でセッター関数を呼び出してクリアすることができます。
よくある落とし穴とベストプラクティス
useFormStatus
の配置:useFormStatusを呼び出すコンポーネントは、`<form>`の子としてレンダリングされなければならないことを忘れないでください。兄弟や親である場合は機能しません。- シリアライズ可能な状態:サーバーアクションを使用する場合、アクションから返される状態オブジェクトはシリアライズ可能でなければなりません。これは、関数やシンボル、その他のシリアライズ不可能な値を含めることができないことを意味します。プレーンなオブジェクト、配列、文字列、数値、ブール値に固執してください。
- アクション内でスローしない:`throw new Error()`の代わりに、アクション関数はエラーを適切に処理し、エラーを記述する状態オブジェクト(例:`{ success: false, message: 'エラーが発生しました' }`)を返す必要があります。これにより、状態が常に予測どおりに更新されることが保証されます。
- 明確な状態の形状を定義する:最初から状態オブジェクトのための一貫した構造を確立します。`{ data: T | null, message: string | null, success: boolean, errors: Record
| null }`のような形状は、多くのユースケースをカバーできます。
useActionState vs. useReducer: 簡単な比較
一見すると、useActionStateはuseReducerに似ているように見えるかもしれません。どちらも前の状態に基づいて状態を更新することに関わるからです。しかし、それらは異なる目的を果たします。
useReducer
は、クライアントサイドで複雑な状態遷移を管理するための汎用フックです。アクションをディスパッチすることでトリガーされ、多くの同期的な状態変化の可能性がある状態ロジック(例:複雑なマルチステップウィザード)に最適です。useActionState
は、単一の、通常は非同期のアクションに応答して変化する状態のために設計された特殊なフックです。その主な役割は、HTMLフォーム、サーバーアクション、および保留状態の遷移のようなReactのコンカレントレンダリング機能と統合することです。
要点:フォーム送信やフォームに結びついた非同期操作には、useActionStateが現代的で目的に特化したツールです。その他の複雑なクライアントサイドの状態管理には、useReducerが引き続き優れた選択肢です。
結論:Reactフォームの未来を受け入れる
useActionStateフックは単なる新しいAPI以上のものであり、Reactでフォームとデータミューテーションを扱うための、より堅牢で、宣言的で、ユーザー中心の方法への根本的なシフトを表しています。それを採用することで、次の利点が得られます:
- ボイラープレートの削減:単一のフックが、複数のuseState呼び出しと手動の状態管理を置き換えます。
- 統合された保留状態:コンパニオンフックであるuseFormStatusを使用して、ローディングUIをシームレスに処理します。
- 組み込みのプログレッシブエンハンスメント:JavaScriptの有無にかかわらず機能するコードを記述し、すべてのユーザーのアクセシビリティと回復力を確保します。
- 簡素化されたサーバー通信:サーバーアクションに自然に適合し、フルスタック開発体験を合理化します。
新しいプロジェクトを開始したり、既存のものをリファクタリングしたりする際には、useActionStateの採用を検討してください。それは、コードをよりクリーンで予測可能にすることで開発者体験を向上させるだけでなく、より速く、より回復力があり、多様なグローバルオーディエンスがアクセスできる高品質なアプリケーションを構築する力を与えてくれるでしょう。