🍙

Server Actions の結果をクライアント側の状態に反映させる方法

2024/04/20に公開

問題

server actionで処理した結果をクライアント側にその結果を渡して、反映させることができなかった。

問題のコード

import { revalidatePath } from "next/cache";
import { ResultBox } from "./Result";

#処理方法
async function toUppercase(text: string): Promise<string> {
  const processedText = text.toUpperCase();
  return processedText;
}

#action
async function convert(formData: FormData) {

#server側の処理にしたいからuser serverと書きました
  "use server";
#フォームから入力されたデータを取得する
  const text = formData.get("inputText") as string;
  if (!text) {
    return { error: "Please enter some text to process." };
  }
  console.log("Processing text:", text);

#toUppercaseの処理をする
  const result = await toUppercase(text);
  revalidatePath("/");
  return { result };
}

export default function Home() {
#actionで得た結果をここで取得したいが、結果にアクセスできていない
  const resultText = convert.result?.result;

  return (
    <main className="flex min-h-screen flex-col items-center justify-center p-24">
      <div className="relative flex flex-col place-items-center before:absolute">
        <h1 className="mb-6 text-3xl font-bold">やさしい日本語変換</h1>
        <p className="mb-6 text-lg font-bold">
          むずかしい日本語を、わかりやすく・よみやすい、やさしい日本語に変換します。
        </p>
        <div className="flex items-start">
          <form action={convert} className="flex flex-col items-center mr-4">
            <textarea
              name="inputText"
              className="mb-4 rounded-md"
              placeholder="日本語を入力してください"
            ></textarea>
            <button
              type="submit"
              className="button rounded-lg border border-transparent px-5 py-4"
            >
              <h2 className="text-xl font-semibold">へんかん!</h2>
            </button>
          </form>
#ここに結果を表示させたい↓
          <ResultBox result={resultText} />
        </div>
      </div>
    </main>
  );
}

原因

1つのファイルのサーバーコンポーネントに、"use client"と明記せずに、クライアント側の処理を書いており、useStateのような状態を管理するフックを使うことができないようになっていました。

解決策

サーバーとクライアント側のコンポーネントファイルを分ける
分けない場合は、"use client"と明記する

next.jsのapp routerの場合、デフォルトで全てのコンポーネントがサーバーコンポーネントになっています。各ファイルの1行目に"use client"と定義する事で、クライアントコンポーネントにすることができます。解決したコードでは、サーバー側とクライアント側での処理でファイルを分けました。そうすることで、サーバー側の処理をインポートして使用し、処理したデータをクライアント側でuseStateを使い状態管理をし表示させることができました。

解決したコード

#クライアントコンポーネントにするように明記している
"use client";

import { useState } from "react";
import ResultBox from "./components/ResultBox";
import toUppercase from "./actions/toUppercase";

#型設定
interface CommonActionsReturn {
  success: boolean;
  message: string;
  data: string;
}

export default function Home() {
  const convert = async (formData : FormData) => {
#入力値にアクセス
    const text : FormDataEntryValue | null = formData.get("inputText")

#別のファイルに分けて書いたserver actionの処理をインポートして使用
    const result : CommonActionsReturn = await toUppercase(text);

    if(result.success) {
      setResultText(result.data);
    }else{
      // エラー処理
    }
  }

#得られた結果の状態管理
  const [resultText, setResultText] = useState<string>('');

  return (
    <main className="flex min-h-screen flex-col items-center justify-center p-24">
      <div className="relative flex flex-col place-items-center before:absolute">
        <h1 className="mb-6 text-3xl font-bold">やさしい日本語変換</h1>
        <p className="mb-6 text-lg font-bold">
          むずかしい日本語を、わかりやすく・よみやすい、やさしい日本語に変換します。
        </p>
        <div className="flex items-start">
          <form action={convert} className="flex flex-col items-center mr-4">
            <textarea
              name="inputText"
              className="mb-4 rounded-md"
              placeholder="日本語を入力してください"
            ></textarea>
            <button
              type="submit"
              className="button rounded-lg border border-transparent px-5 py-4"
            >
              <h2 className="text-xl font-semibold">へんかん!</h2>
            </button>
          </form>
          <ResultBox result={resultText} />
        </div>
      </div>
    </main>
  );
}
// toUppercase.ts
#サーバーコンポーネントにすることを明記
"use server";

interface CommonActionsReturn {
    success: boolean;
    message: string;
    data: string;
}

export default async function toUppercase (text : FormDataEntryValue | null) : Promise<CommonActionsReturn> {
    // 実際の処理
    if(!text) return { success : false, message : 'Please enter some text to process.', data : '' };

    const _text : string = (text as string).toUpperCase();

    return { success : true, message : 'success', data: _text };
}

よく分かっていないところ

FormDataEnterValueとは何か
すでに定義された変数かと調べたがよく分からなかった。

const text : FormDataEntryValue | null = formData.get("inputText")
export default async function toUppercase (text : FormDataEntryValue | null) : Promise<CommonActionsReturn> {
    // 実際の処理
    if(!text) return { success : false, message : 'Please enter some text to process.', data : '' };

    const _text : string = (text as string).toUpperCase();

    return { success : true, message : 'success', data: _text };
}

Discussion