📝

React19で追加されたhooks触ってみた(useActionState)

2025/02/01に公開

useActionState

基本的には、useReducerを非同期にしたものに近いが、少しだけ違う。
useActionStateは基本的にform要素と一緒に使用する。

定義

const [state, formAction, isPending] = useActionState(fn, initialState, permalink?);

引数

fn

返り値のformActionを実行した際に呼び出される関数。
1つ目の引数は、前回のstate(初回はinitialState)を受け取る。
2つ目の引数は、基本的に任意の型を受け取る。FormDataを指定した場合は、formAction属性に直接渡すことができる。
返り値は、stateまたはPromise<state>を返す。
そのため、非同期関数を引数として渡すこともできる。

initialState

初期値として使用する値。

permalink(オプション)

フォームが書き換えの対象とするユニークなページ URL を含んだ文字列。

返り値

state

現在のstateを返し、初回レンダー時は、initialStateに渡した値が設定される。

formAction

useReducerdispatch関数と同じような関数。
引数として、引数として渡したfnの第2引数と同じ型を受け取る。

isPending

formActionが実行中かどうかを返す。
form要素を使用せずにbutton要素のonClickに値を設定して、formActionを実行すると、formActionが実行中でも、値がずっとfalseになる。

1. FormData型を使用

const sleep = (ms: number): Promise<void> =>
  new Promise((resolve) => setTimeout(resolve, ms));

type User = {
  id: number;
  name: string;
};

const addUser = async (state: User[], formData: FormData) => {
  await sleep(1000);
  const id = state.length + 1;
  const name = formData.get("name");
  const newUser = {
    id,
    name,
  };

  return [...state, newUser] as User[];
};

const Component = () => {
  const [state, formAction, isPending] = useActionState(addUser, [] as User[]);

  return (
    <div>
      <div>{isPending ? "loading..." : ""}</div>
      <ul>
        {state.map((user) => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
      <form action={formAction}>
        <input type="text" name="name" />
        <button disabled={isPending} type="submit">
          送信
        </button>
      </form>
    </div>
  );
};

2. 任意の型(FormData以外)を使用


const sleep = (ms: number): Promise<void> =>
  new Promise((resolve) => setTimeout(resolve, ms));

type User = {
  id: number;
  name: string;
};

const addUser = async (prev: User[], userName: string) => {
  await sleep(1000);
  const id = prev.length + 1;
  const newUser = {
    id,
    name: userName,
  };

  return [...prev, newUser];
};

const Component = () => {
  const [state, formAction, isPending] = useActionState(addUser, [] as User[]);
  const inputRef = useRef<HTMLInputElement>(null);

  const handleClick = () => {
    formAction(inputRef.current?.value ?? "");
  };

  return (
    <div>
      <div>{isPending ? "loading..." : ""}</div>
      <ul>
        {state.map((user) => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
      <form action={handleClick}>
        <input type="text" name="name" ref={inputRef} />
        <button disabled={isPending} type="submit">
          送信
        </button>
      </form>
    </div>
  );
};

感想

以前までは、以下のように自分で呼び出し中などをuseStateで管理する必要があった。
useActionStateが追加されたことで、form要素を使用する必要はあるが、呼び出し中などの管理を自分で行う必要がなく、かなり便利なhooksだと感じた。

const sleep = (ms: number): Promise<void> =>
  new Promise((resolve) => setTimeout(resolve, ms));

type User = {
  id: number;
  name: string;
};

const addUser = async (prev: User[], userName: string) => {
  await sleep(1000);
  const id = prev.length + 1;
  const newUser = {
    id,
    name: userName,
  };

  return [...prev, newUser];
};

const Component = () => {
  const [isPending, setIsPending] = useState(false);
  const [state, setState] = useState([] as User[]);

  const inputRef = useRef<HTMLInputElement>(null);

  const handleClick = async () => {
    setIsPending(true);
    const users = await addUser(state, inputRef.current?.value ?? "");
    setState(users);
    setIsPending(false);
  };

  return (
    <div>
      <div>{isPending ? "loading..." : ""}</div>
      <ul>
        {state.map((user) => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
      <input type="text" name="name" ref={inputRef} />
      <button disabled={isPending} type="button" onClick={handleClick}>
        送信
      </button>
    </div>
  );
};

参考

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

Discussion