🌟

React19のuseActionsStateをTypeScriptで書く

2024/06/18に公開

注意!!

2024/06/18現在、まだRC版なので使い方が変わる可能性があることを留意してください。

公式サンプル(JavaScript版)

元記事
https://react.dev/blog/2024/04/25/react-19

// Using <form> Actions and useActionState
function ChangeName({ name, setName }) {
  const [error, submitAction, isPending] = useActionState(
    async (previousState, formData) => {
      const error = await updateName(formData.get("name"));
      if (error) {
        return error;
      }
      redirect("/path");
      return null;
    },
    null,
  );

  return (
    <form action={submitAction}>
      <input type="text" name="name" />
      <button type="submit" disabled={isPending}>Update</button>
      {error && <p>{error}</p>}
    </form>
  );
}

解説

TypeScriptだと、previousStateの型を、前回の値とエラー時に返す値で同じ型で表現する必要があり、まとめる必要が出てくる。

前回submitボタン押した時のデータが必要ない場合はまとめなくてよい。

公式のサンプルは、previousStateと書かなければ混乱しなかったのでは?とも思う。
(formには別途普通のstateを使うと思うし。。。)

前回の値を使う場合(前回の値とエラー型を保持できる型を指定する場合)

import { useState, useActionState } from 'react'

// コピペで動かせるように適当に関数を設定
const updateName = async (name: string) => `${name}を更新できません`

// エラーだけの型定義だと前回の値が取得できないので、データも一緒に定義する
interface ActionStateType {
  data: { [key: string]: string }
  error: string | null
}

function ChangeName() {
  // submit時に値がクリアされてしまうためstateを使うが
  // その場合は、formDataの値を使う必要もなくなる気がして正しいのか不明
  const [name, setName] = useState('')

  const [actionState, submitAction, isPending] = useActionState(
    async (
      previousActionData: ActionStateType,
      formData: FormData,
    ): Promise<ActionStateType> => {
      // 前回submit時にreturnしたデータが入ってくる(初回は初期値)
      console.log(previousActionData)
      const fname = formData.get('name') as string
      const error = await updateName(fname)
      if (error) {
        return { data: { name: fname }, error }
      }
      return { data: { name: fname }, error: null }
    },
    // ここで初期データを指定しておかないと初回のsubmit時に前回の状態が正しく取れない
    { data: { name: "" }, error: null },
  )

  return (
    <form action={submitAction}>
      <input
        type="text"
        name="name"
        value={name}
        onChange={(e) => setName(e.target.value)}
      />
      <button type="submit" disabled={isPending}>
        Update
      </button>
      {actionState.error && <p>{actionState.error}</p>}
    </form>
  )
}

前回の値を使わない場合(エラー型のみ指定する場合)

import { useState, useActionState } from 'react'

// コピペで動かせるように適当に関数を設定(ボタンのdisableも見れるようにdelayする)
const updateName = async (name:string) => new Promise((resolve) => {
  setTimeout(() => resolve(name), 3000)
})

function ChangeName() {
  const [error, submitAction, isPending] = useActionState(
    async (
      _previousError: string | null,
      formData: FormData
    ): Promise<string | null> => {
      const error = await updateName(formData.get("name") as string);
      if (error) {
        return error;
      }
      return null;
    },
    null,
  );

  return (
    <form action={submitAction}>
      <input type="text" name="name" />
      <button type="submit" disabled={isPending}>Update</button>
      {error && <p>{error}</p>}
    </form>
  );
}

submit前の値を使わないほうが、エラー型のみとなるのでスッキリする。
(スッキリしているのは、stateの記述を割愛したのもあるとは思われる)

ひとこと

React18よりもシンプルに書けるようになったので良いですね^^

Discussion