Open7

Next Server Actions パターンまとめ

shiratorishiratori

サーバーコンポーネント内にインラインで記述して実行する

src/app/server-actions/server-form-submit-inline/form.tsx
import { InputText } from '@/components/form/InputText';
import { SubmitButton } from '@/components/form/SubmitButton';

export const Form = () => {
  async function createInvoice(formData: FormData) {
    /** インラインで書けるのはサーバーコンポーネントの場合のみ */
    'use server';

    const rawFormData = {
      customerId: formData.get('customerId'),
      amount: formData.get('amount'),
      status: formData.get('status'),
    };

    console.log('createInvoice: ', { rawFormData });

    // mutate data
    // revalidate cache
  }

  return (
    <form action={createInvoice}>
      <InputText name="customerId" />
      <InputText name="amount" />
      <InputText name="status" />
      <SubmitButton label="送信" />
    </form>
  );
};

shiratorishiratori

クライアントコンポーネントのフォームサブミットで実行

src/app/server-actions/client-form-submit/actions.ts
'use server';

export async function createAction(formData: FormData) {
  console.log('createAction formData: ', formData);
}

src/app/server-actions/client-form-submit/Form.tsx
'use client';

import { SubmitButton } from '@/components/form/SubmitButton';
import { InputText } from '@/components/form/InputText';
import { useState } from 'react';
import { ReturnState, somethingUpdate } from './actions';
import { useFormState } from 'react-dom';

const initialState: ReturnState = {
  message: '',
};

export const Form = () => {
  const [state, formAction] = useFormState(somethingUpdate, initialState);

  return (
    <div>
      <form action={formAction}>
        <InputText name="customerId" />
        <InputText name="amount" />
        <InputText name="status" />
        <SubmitButton label="送信" />
      </form>

      <div>`state.message: ` {state?.message}</div>
    </div>
  );
};

shiratorishiratori

クライアントコンポーネントのフォームサブミットで実行する際にbind()で引数を追加する

src/app/server-actions/client-form-submit-bind/actions.ts
'use server';

export async function updateAction(userId: string, formData: FormData) {
  console.log('updateAction: ', { userId, formData });
}

src/app/server-actions/client-form-submit-bind/Form.tsx
'use client';

import { SubmitButton } from '@/components/form/SubmitButton';
import { InputText } from '@/components/form/InputText';
import { updateAction } from './actions';

type Props = {
  userId: string;
};

export const Form = ({ userId }: Props) => {
  const updateUserWithId = updateAction.bind(null, userId);

  return (
    <form action={updateUserWithId}>
      <InputText name="customerId" />
      <InputText name="amount" />
      <InputText name="status" />
      <SubmitButton label="送信" />
    </form>
  );
};

shiratorishiratori

クライアントコンポーネントのフォームサブミットでuseFormStateを使用する

src/app/server-actions/client-form-useFormState/actions.ts
'use server';

import { fakeFetch } from '@/utils/fakeFetch';

export type ReturnState = {
  message: string;
};

export async function somethingUpdate(
  prevState: any,
  formData: FormData
): Promise<ReturnState> {
  console.log('somethingUpdate: ', { prevState, formData });

  try {
    const res = await fakeFetch<FormData, undefined>({
      reqBody: formData,
      delayTime: 500,
    }).catch((error) => {
      throw new Error(error);
    });
    console.log({ res });
    return {
      message: 'somthingAction successfully',
    };
  } catch (e) {
    throw new Error('fetch error');
  }
}


src/app/server-actions/client-form-useFormState/Form.tsx
'use client';

import { SubmitButton } from '@/components/form/SubmitButton';
import { InputText } from '@/components/form/InputText';
import { createAction } from './actions';

export const Form = () => {
  return (
    <form action={createAction}>
      <InputText name="customerId" />
      <InputText name="amount" />
      <InputText name="status" />
      <SubmitButton label="送信" />
    </form>
  );
};

shiratorishiratori

フォームを使わずにボタンクリックで実行する

src/app/server-actions/client-button/actions.ts
'use server';

import { fakeFetch } from '@/utils/fakeFetch';

export type LikeState = {
  likes: number;
};

export async function incrementLike(prevState: number): Promise<LikeState> {
  console.log('incrementLike: ', { prevState });

  try {
    const res = await fakeFetch<LikeState, LikeState>({
      reqBody: { likes: prevState },
      resBody: { likes: prevState + 1 },
      delayTime: 500,
    }).catch((error) => {
      throw new Error(error);
    });
    console.log({ res });
    return (
      res?.response ?? {
        likes: 0,
      }
    );
  } catch (e) {
    throw new Error('fetch error');
  }
}

src/app/server-actions/client-button/LikeButton.tsx
'use client';

import { useState } from 'react';
import { incrementLike } from './actions';
import { Button } from '@/components/Button';

type Props = { initialLikes: number };

export const LikeButton = ({ initialLikes }: Props) => {
  const [likes, setLikes] = useState(initialLikes);

  return (
    <>
      <p>Total Likes: {likes}</p>
      <Button
        label="like"
        onClick={async () => {
          const updatedLikes = await incrementLike(likes);
          setLikes(updatedLikes.likes);
        }}
      />
    </>
  );
};

shiratorishiratori

useActionStateについて

以下のバージョンで使うとエラーになる

"next": "14.2.4",
"react": "^18",
"react-dom": "^18"
TypeError: (0 , react__WEBPACK_IMPORTED_MODULE_3__.useActionState) is not a function or its return value is not iterable
    at Form (./src/app/server-actions/client-form-useFormState/Form.tsx:21:97)
digest: "4015484258"
  12 | export const Form = () => {
  13 |   const [state, formAction, isPending] = useActionState(
> 14 |     somethingUpdate,
     |     ^
  15 |     initialState
  16 |   );
  17 |
 ✓ Compiled in 429ms (263 modules)

reactとnextのバージョンをcanaryにしないと使えないらしい
https://github.com/vercel/next.js/issues/65673#issuecomment-2106667446

"dependencies": {
  "next": "14.3.0-canary.59",
  "react": "19.0.0-beta-4508873393-20240430",
  "react-dom": "19.0.0-beta-4508873393-20240430"
}

https://github.com/vercel/next.js/issues/65673#issuecomment-2108948359

次期バージョンの安定版で動作しないコードをドキュメントに掲載するのは非常に奇妙だ。

https://github.com/vercel/next.js/issues/65673#issuecomment-2112065178

ちょうど同じ問題にぶつかったところだ。 next-formsの例にあるパッケージは、それがカナリアリリースであることに触れておらず、ただ'最新'バージョンを使っている。

https://github.com/vercel/next.js/issues/65673#issuecomment-2146873018

そうなんです。 でもとにかく、Next.jsが正式に非推奨のものとしてマークするまで、私はまだuseFormState()を使い続けるつもりです。 reactのバージョン18とNext.jsのバージョン14.2.3を使っても、このエラーが出ます。