【Next.js】useTransitionを使ったServer Actionsの二重押下防止
はじめに
Next.jsで個人開発を行なっている中で更新ボタンを何度も押せてしまう!という致命的なバグを検出し修正したので記事にしました。
更新処理はServer Actionsを使用しています。
Server Actionsの任意の状態(今回はエラー有無やメッセージ)をuseFormState
で、更新処理の進行状態をuseTransition
で管理しました。
使用しているバージョン
"next": "^14.2.4",
"react": "^18.3.1",
useFormState
Server ActionsとServer ActionsはNext.js 13.4以降で導入された機能で、フォームの送信やデータの更新などのサーバーサイド処理をClient Componentsから直接呼び出すことができます。
useFormState
はアクションの状態を管理するためのフックです。
useTransition
とは
useTransition
は、React 18で導入された新しいフックで、UIをブロックせずに状態更新を行うためのものです。たとえば、大量のデータをレンダリングする際などの重い処理が発生すると、操作が遅く感じることがあります。
useTransition
を使用することで、重い処理をバックグラウンドで実行し、ユーザー操作を妨げずにスムーズに動作するようにできます。
const [isPending, startTransition] = useTransition();
useTransition
は特にパラメーターをとりません。isPending
とstartTransition
を含む配列を返却します。
startTransition
は状態更新の遅延処理をラップして使います。
isPending
は、startTransition
によって実行されている遅延状態更新がまだ処理中であるかどうかを示します。isPending
がtrue
の場合、バックグラウンド処理が完了していないことを意味し、false
であれば遅延状態更新が完了していることを示します。
次のように使用します。
function TabContainer() {
const [isPending, startTransition] = useTransition();
const [tab, setTab] = useState('about');
function selectTab(nextTab) {
// 遅延処理をラップして実行
startTransition(() => {
setTab(nextTab);
});
}
// ...
}
手順
useTransition
を使用してServer Actionsの二重押下を防止する手順を説明します。
- フォーム送信時のActions関数を作成
- useFormStateでServer Actionsの状態を管理
- useTransitionを追加し、状態更新の遅延処理をラップする
1.フォーム送信時のActions関数を作成
まず、フォーム送信時に実行されるサーバーサイドの処理(Actions関数)を作成します。この関数は、フォームの入力内容を処理し、必要な更新処理をサーバー側で実行します。
"use server";
import { z } from "zod";
import { api } from "@/trpc/server";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
export type State = {
message: string;
error: boolean;
};
const FormSchema = z.object({
memo: z.string(),
});
export async function updateAction(prevState: State, formData: FormData) {
// フォームの値を取得
const { memo } = FormSchema.parse({
memo: formData.get("memo"),
});
// サーバー側の更新処理
try {
await api.article.update({ memo });
revalidatePath("/");
} catch (error) {
console.error("Error fetching:", error);
return {
message: "保存に失敗しました",
error: true,
};
}
redirect("/");
}
useFormState
でServer Actionsの状態を管理
2.次に、クライアント側でフォームの状態管理を行います。useFormState
を使用することで、フォームのエラーメッセージや状態を管理し、フォームの送信時にServer Actionsを実行します。
"use client";
import React from "react";
import { useFormState } from "react-dom";
import { updateAction } from "@/lib/updateAction";
export const ArticleMemo = () => {
const [state, updateActionState] = useFormState(updateAction, {
message: "",
error: false,
});
return (
<>
<form action={updateActionState}>
<textarea id="memo" name="memo" rows={6} />
{/* 更新ボタン */}
<div className="flex justify-end">
<button type="submit">保存</button>
</div>
</form>
</>
);
};
この状態でフォーム送信は可能ですが、現在のままでは、処理中にボタンを二重に押下することができてしまいます。次に、useTransition
を使って、これを防ぎます。
useTransition
を追加し、状態更新の遅延処理をラップする
3.ReactのuseTransition
を使用することで、ボタンを非活性にし二重押下を防止しつつ、処理中にローディング表示を行います。
"use client";
+ import React, { useTransition } from "react";
import { useFormState } from "react-dom";
import { updateAction } from "@/lib/updateAction";
+ import { LoadingOverlay } from "./LoadingOverlay";
export const ArticleMemo = () => {
const [state, updateActionState] = useFormState(updateAction, {
message: "",
error: false,
});
+ const [isPending, startTransition] = useTransition();
+ const hundleSubmit = (formData: FormData) => {
+ startTransition(() => updateActionState(formData));
+ };
return (
<>
- <form action={updateActionState}>
+ <form action={hundleSubmit}>
<textarea id="memo" name="memo" rows={6} />
{/* 更新ボタン */}
<div className="flex justify-end">
- <button type="submit">保存</button>
+ <button type="submit" disabled={isPending}>
+ {isPending ? "保存中..." : "保存"}
+ </button>
</div>
</form>
+ {isPending && <LoadingOverlay />}
</>
);
};
useTransition
はReactのトップレベルで宣言し、isPending
とstartTransition
を含む配列を受け取ります。
ボタン押下時の処理はhundleSubmit
関数を作成し、フォームのaction属性に設定します。フォームの入力値はformData
で受け取り、それをupdateActionState
に渡します。
さらに、updateActionState
の処理をstartTransition
でラップすることで、遅延処理の状態を管理でき、isPending
によって処理中であることを確認できます。
isPending
がtrue
のときは、ローディングを表示したり、ボタンを非活性にすることで、二重押下を防止することができます。
まとめ
いかがだったでしょうか。
今回はuseTransitionを使ったServer Actionsの二重押下防止について紹介しました!
登録・更新系ボタンが何度も押せるのはよくないですね、、、!
少しでも参考になれば嬉しいです!
Discussion
useFormState()
ではなく、useFormStatus()
ご存知でしょうか?特別な事情などなければこちらの方が適切かもしれません。
存在は知っていましたが中身までは理解していませんでした!
試してみようと思います!ありがとうございます!