🍁

【Next.js】useTransitionを使ったServer Actionsの二重押下防止

2024/09/28に公開2

はじめに

Next.jsで個人開発を行なっている中で更新ボタンを何度も押せてしまう!という致命的なバグを検出し修正したので記事にしました。

更新処理はServer Actionsを使用しています。
Server Actionsの任意の状態(今回はエラー有無やメッセージ)をuseFormStateで、更新処理の進行状態をuseTransitionで管理しました。

使用しているバージョン
"next": "^14.2.4",
"react": "^18.3.1",

Server ActionsとuseFormState

Server ActionsはNext.js 13.4以降で導入された機能で、フォームの送信やデータの更新などのサーバーサイド処理をClient Componentsから直接呼び出すことができます。
useFormStateはアクションの状態を管理するためのフックです。

https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations

https://ja.react.dev/reference/react/useActionState

useTransitionとは

useTransitionは、React 18で導入された新しいフックで、UIをブロックせずに状態更新を行うためのものです。たとえば、大量のデータをレンダリングする際などの重い処理が発生すると、操作が遅く感じることがあります。
useTransitionを使用することで、重い処理をバックグラウンドで実行し、ユーザー操作を妨げずにスムーズに動作するようにできます。
https://react.dev/reference/react/useTransition

  const [isPending, startTransition] = useTransition();

useTransitionは特にパラメーターをとりません。isPendingstartTransitionを含む配列を返却します。

startTransitionは状態更新の遅延処理をラップして使います。
isPendingは、startTransitionによって実行されている遅延状態更新がまだ処理中であるかどうかを示します。isPendingtrueの場合、バックグラウンド処理が完了していないことを意味し、falseであれば遅延状態更新が完了していることを示します。

次のように使用します。

function TabContainer() {
  const [isPending, startTransition] = useTransition();
  const [tab, setTab] = useState('about');

  function selectTab(nextTab) {
    // 遅延処理をラップして実行
    startTransition(() => {
      setTab(nextTab);
    });
  }
  // ...
}

手順

useTransitionを使用してServer Actionsの二重押下を防止する手順を説明します。

  1. フォーム送信時のActions関数を作成
  2. useFormStateでServer Actionsの状態を管理
  3. useTransitionを追加し、状態更新の遅延処理をラップする

1.フォーム送信時のActions関数を作成

まず、フォーム送信時に実行されるサーバーサイドの処理(Actions関数)を作成します。この関数は、フォームの入力内容を処理し、必要な更新処理をサーバー側で実行します。

lib/updateActions.ts
"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("/");
}

2.useFormStateでServer Actionsの状態を管理

次に、クライアント側でフォームの状態管理を行います。useFormStateを使用することで、フォームのエラーメッセージや状態を管理し、フォームの送信時にServer Actionsを実行します。

components/ArticleMemo.tsx
"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を使って、これを防ぎます。

3.useTransitionを追加し、状態更新の遅延処理をラップする

ReactのuseTransitionを使用することで、ボタンを非活性にし二重押下を防止しつつ、処理中にローディング表示を行います。

components/ArticleMemo.tsx
"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のトップレベルで宣言し、isPendingstartTransitionを含む配列を受け取ります。

ボタン押下時の処理はhundleSubmit関数を作成し、フォームのaction属性に設定します。フォームの入力値はformDataで受け取り、それをupdateActionStateに渡します。

さらに、updateActionStateの処理をstartTransitionでラップすることで、遅延処理の状態を管理でき、isPendingによって処理中であることを確認できます。

isPendingtrueのときは、ローディングを表示したり、ボタンを非活性にすることで、二重押下を防止することができます。

まとめ

いかがだったでしょうか。
今回はuseTransitionを使ったServer Actionsの二重押下防止について紹介しました!
登録・更新系ボタンが何度も押せるのはよくないですね、、、!
少しでも参考になれば嬉しいです!

Discussion