前書き
useActionStateを使ってサーバーサイドの処理完了後、クライアントにどのように通知すれば良いのか、自分なりの知見を共有できればと思います。
NextJS | react |
---|---|
15.1.7 | 19.0.0 |
useActionStateとは
React 19ではuseActionStateという新しいフックが導入されました。
これは以前、react-domの一部としてuseFormStateという名前で存在していました。
基本的な使い方
useActionState
フックは次のような形式で使用します:
const [state, runAction, isPending] = useActionState(
action,
initialState,
);
- state: アクションの実行結果が格納される状態
- runAction: アクションを実行するための関数
- isPending: アクションが実行中かどうかを示すブール値
- action:Server Action関数を指定します
- initialState:stateの初期値
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>
)
}
そもそもトースト通知を使わない
ふざけた話に聞こえますが、ユースケースは存在します。
「投稿
→ 投稿後の中間ページ
→ 投稿内容確認
」のような流れであれば、トースト通知は冗長になる可能性があります。
ページ遷移自体がアクションの成功を暗示するため、追加の通知も不要かもしれません。
実装方法
Next.jsでこのパターンを実装するのは非常にシンプルです。Server Actionを使って以下のコードを実装します:
export async function handleSubmission(formData: FormData) {
// データ処理ロジック
await saveData(formData);
// キャッシュを更新してリダイレクト
revalidatePath("/confirmation-page");
redirect("/confirmation-page");
}
このアプローチでは、フォーム送信後に:
- 関連するパスのキャッシュがクリアされ、最新のデータが表示される
- ユーザーは自動的に中間/確認ページにリダイレクトされる
これにより、トースト通知を使わなくても、ユーザーは操作の結果を明確に理解できます。
useEffectを使ってモニタリングする
useActionStateを使用する時、actionの戻り値によって、
stateの値も変わります、その変更をuseEffectを使って、モニタリングをします。
実装方法
// clients.ts
'use client';
import { useEffect } from 'react';
import { useActionState } from 'next/client';
import { handleSubmission } from './actions';
export default function FormComponent() {
const [state, runAction, isPending] = useActionState(
handleSubmission,
{ status: null, message: '' }
);
// stateの変更を監視
useEffect(() => {
if (state.status === "success") {
// 成功時の処理
toast.success("送信が完了しました");
// フォームのリセットやリダイレクトなど
} else if (state.status === "error") {
// エラー時の処理
toast.error(state.message || "エラーが発生しました");
}
}, [state]);
async function onSubmit(formData: FormData) {
await runAction(formData);
}
return (
<form action={onSubmit}>
{/* フォームフィールド */}
<button disabled={isPending}>
{isPending ? '送信中...' : '送信'}
</button>
</form>
);
}
// actions.ts
export async function handleSubmission(formData: FormData) {
...
// 何かの戻り値
return {status: "success", message: ''}
}
カスタマフックを利用する
各クライアントコンポーネントで毎回useEffect
を書くのは面倒なので、これをうまくフックとして抽出し、カスタムフックとして管理するのはいいのではないかと考えてます
実装方法
まず、コールバック関数を処理するためのユーティリティ関数を作成します:
type State = { status: string | null; message: string };
export type CallbacksConfig = {
onError?: () => void;
onSuccess?: () => void;
};
export function withCallbacks(
action: (actionState: State, formData: FormData) => Promise<State>,
callbacks: CallbacksConfig,
) {
return async (actionState: State, formData: FormData) => {
const result = await action(actionState, formData);
if (result.status === 'error') {
callbacks.onError?.(lastResult);
} else if (result.status === 'success') {
callbacks.onSuccess?.(lastResult);
}
return result;
};
}
次に、カスタムフックを作成してuseActionStateと組み合わせます:
import { useActionState } from 'react';
import { withCallbacks } from './use-with-callbacks';
import { handleSubmission } from './actions';
const useHandleSubmission = () => {
return useActionState(
withCallbacks(handleSubmission, {
onError: () => {
toast.error("エラーが発生しました");
},
onSuccess: () => {
toast.success("送信が完了しました");
},
}),
{ status: null, message: '' },
);
};
export default useHandleSubmission;
これで、クライアントコンポーネントでは次のように簡潔に使用できます:
import useHandleSubmission from './useHandleSubmission';
export default function FormComponent() {
const [state, runAction, isPending] = useHandleSubmission();
async function onSubmit(formData: FormData) {
await runAction(formData);
}
return (
<form action={onSubmit}>
{/* フォームフィールド */}
<button disabled={isPending}>
{isPending ? '送信中...' : '送信'}
</button>
</form>
);
}
最後
AIに指示しても、useActionState
に関して、十分に納得できる回答は得られませんでした。
React 19が正式にリリースされたのは2024年12月5日ですが、現時点(2025年3月)では主要モデルたちがまだ十分に学習しきれていないようです。AIが完璧な回答を提供できる日が来るまで、この記事がどなたかのお役に立てれば幸いです。
参考内容