0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

トナカイ 一頭Advent Calendar 2024

Day 6

useActionStateでpending時のブロッキングとエラーメッセージを出す

Last updated at Posted at 2024-12-07

useActionStateとは

export const FormSample: FC = () => {
  const [state, action, isPending] = useActionState<NameAgeState, FormData>(someAction, initialState);
  <form action={action}>
...

このように、stateとactionとisPendingを返します。
それぞれの用途は

state

  • errorMessageを紐づけて、インタラクティブに表示させる
    • useStateでerrorMessageをつけるのを、このstateに統一可能

action

  • formタグのactionに紐づける

isPending

  • formの処理中、クライアントのボタンクリックによる再送信をブロッキング

with TypeScript

const [state, action, isPending] = useActionState<NameAgeState, FormData>(someAction, initialState)

someAction

  • 非同期関数
async function someAction(previousState:NameAgeState , formData:FormData):Promise<NameAgeState> {...

formタグにuseActionStateactionを紐づける関係で、第二引数はformData固定?

initialState

  • 初期値
    • formDataではなく、object

NameAgeState(第一ジェネリクス)

  • 更新されるstateの型

FormData(第二ジェネリクス)

  • 型情報ではPayloadだったが、formタグにuseActionStateactionを紐づける関係で、第二引数はFormData固定?

コードサンプル

FormSample.tsx
"use client";

import { FC, useActionState } from "react";
import { z } from 'zod';

const initialState = {name_input: '', age_input: 20}
type InitialStateType = typeof initialState;
interface NameAgeState extends InitialStateType {
  errorMessage?: {name_input?: string[]|undefined, age_input?: string[]|undefined}
};
const schema = z.object({
  name_input: z.string().refine(name => !name.includes('@'), {message: 'name is not email'}),
  age_input: z.number().min(3, {message: 'min is 3'}).max(120, {message: 'max is 120.'}).step(1, {message: 'step must be 1.'})
});

type NameAge = {
  name_input: string;
  age_input: number;
};

async function someAction(previousState:NameAgeState , formData:FormData):Promise<NameAgeState> {
  "use server";
  const payload:NameAge = {
    name_input: formData.get('name_input') as string,
    age_input: Number(formData.get('age_input')) as number
  };
  await new Promise(resolve => setTimeout(resolve, 2000));
  const { success, data, error } = schema.safeParse(payload);
  if (success){
    console.log("success")
    return {...data, errorMessage: undefined}
  } else {
    const errorField = error.flatten().fieldErrors
    return {...previousState, errorMessage: errorField}
  };
};

export const FormSample: FC = () => {
  const [state, action, isPending] = useActionState<NameAgeState, FormData>(someAction, initialState);

  return (
    <section>
      <h2><code>Form Action</code> sample.</h2>
      <form id="form-action-sample" name="form_action_sample" action={action}>
        <div>
          <label htmlFor="name-input">名前</label>
          <input type="text" id="name-input" name="name_input" defaultValue={initialState.name_input} disabled={isPending} />
          {!isPending && state.errorMessage?.name_input && state.errorMessage.name_input.map((message, i) => (
            <p key={i}>{message}</p>
          ))}
        </div>
        <div>
          <label htmlFor="age-input">年齢</label>
          <input  type="number" id="age-input" name="age_input" min={3} max={120} defaultValue={initialState.age_input} step={1} disabled={isPending} />
          {!isPending && state.errorMessage?.age_input && state.errorMessage.age_input.map((message, i) => (
            <p key={i}>{message}</p>
          ))}
        </div>
        <button type="submit" disabled={isPending}>送信</button>
      </form>
    </section>
  )
}

export default FormSample
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?