Open7
Next Server Actions パターンまとめ
サーバーコンポーネント内にインラインで記述して実行する
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>
);
};
クライアントコンポーネントのフォームサブミットで実行
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>
);
};
クライアントコンポーネントのフォームサブミットで実行する際に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>
);
};
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>
);
};
フォームを使わずにボタンクリックで実行する
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);
}}
/>
</>
);
};
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にしないと使えないらしい
"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を使っても、このエラーが出ます。