React・新しい書き方とその先

React 19の新しい書き方(Actions)

React 19で入った新しい書き方をまとめます。formのactionに関数を渡す形、送信中と結果を持つuseActionState、待たずに先に反映するuseOptimistic、そしてuseとuseTransition。既存の書き方との関係も整理します。

このコースはReact 19で書いてきましたが、19で入った新しい書き方にはまだ触れていませんでした。ここでまとめます。

急いで覚える必要はありません。(既存の書き方はそのまま動きます。)ただ、新しい記事やライブラリのコードには出てくるので、読めるようにしておくのがこの回の目的です。

<form action={…}>——フォームに関数を渡す

formsの回ではonSubmitpreventDefault()を書きました。React 19では、actionに関数を渡せます

<form action={formAction}>
  <input name="name" />
  <button type="submit">ずかんに追加</button>
</form>

3つ、自動でやってくれます。

  • preventDefault()が要らない(Reactが止める)
  • 入力値はFormDataとして関数に渡る
  • 成功したらフォームが自動でリセットされる

valueonChangeのペアも不要になり、<input name="name" />だけで済みます。

useActionState——送信中と結果を持つ

actionに渡す関数を包むと、結果と送信中の状態が手に入ります。

import { useActionState } from 'react'

type ActionState = { error: string | null }

const [state, formAction, isPending] = useActionState<ActionState, FormData>(
  async (_prevState, formData) => {
    const name = String(formData.get('name') ?? '').trim()
    if (name === '') {
      return { error: '名前を入れてください' }
    }

    await saveBird({ name })      // サーバーに送るつもりの処理
    return { error: null }
  },
  { error: null },                // 初期状態
)

返ってくる3つを使います。

受け取るもの中身
state関数が返した値(エラーメッセージなど)
formAction<form action={…}>に渡すもの
isPending送信中かどうか(自前のstateが不要)
<form action={formAction}>
  <input name="name" />
  <button type="submit" disabled={isPending}>
    {isPending ? '追加中…' : 'ずかんに追加'}
  </button>
  {state.error && <p className="text-red-600">{state.error}</p>}
</form>

useStateが1つも出てきません。 入力値・エラー・送信中を全部自前で持っていたformsの回と比べると、減った量が分かります。

useOptimistic——待たずに先に見せる

サーバーの返事は速くても200ミリ秒かかります。その間、画面が固まって見えるのを避けるのがuseOptimisticです。

import { useOptimistic, startTransition } from 'react'

const [optimisticBirds, addOptimisticBird] = useOptimistic(
  birds,
  (current, newBird) => [...current, { ...newBird, sending: true }],
)

function handleAdd(newBird) {
  startTransition(async () => {
    addOptimisticBird(newBird)   // 先に画面へ足す
    await saveBird(newBird)      // そのあと実際に送る
  })
}

先に画面へ反映して、あとから本当に送ります。 失敗したらReactが自動で元に戻しますbirdsの値に戻る)ので、自分で巻き戻しを書く必要はありません。

useTransition——重い更新で画面を止めない

useTransitionは、急がなくていい更新に印を付ける道具です。

const [isPending, startTransition] = useTransition()

function handleSearch(value) {
  setKeyword(value)                    // 入力欄はすぐ反映(急ぐ)
  startTransition(() => {
    setDeferredKeyword(value)          // 重い一覧の更新は後回しでよい
  })
}

大量の一覧を絞りこむような処理で、入力の反応を優先しつつ、重い更新は追いかけさせることができます。isPendingで「更新中」の印も出せます。

renderingの回で扱った最適化の、もう一つの引き出しです(useMemo計算を減らすのに対し、こちらは優先順位を変える)。

use——PromiseやContextを読む

React 19で入ったuseは、フックのルールから外れた特別なものです。

import { use } from 'react'

function BirdName({ birdPromise }) {
  const bird = use(birdPromise)   // Promiseが解決するまで待つ(Suspenseと組む)
  return <h1>{bird.name}</h1>
}
  • 条件分岐やループの中でも呼べる(他のフックはできない)
  • Promiseを渡すと、解決するまでSuspenseのfallbackが出る
  • Contextも読める(use(BirdsContext)

いま何を使うか

やりたいこといまの推奨
送信するだけのフォーム<form action>useActionState
入力のたびに反応するUIvalueonChange(従来どおり)
複雑な検証つきフォームReact Hook Form+zod(前の回)
いいね・お気に入りの即時反映useOptimistic
一覧の重い更新で入力が引っかかるuseTransition
データ取得TanStack Query(またはフレームワーク)

公式ドキュメントも見てみる

ja.react.dev外部サイトuseActionState – React公式のuseActionState。「フォームアクションと組み合わせる」例が、この回の書き方そのものですja.react.dev外部サイトuseOptimistic – ReactuseOptimisticのリファレンス。失敗したときに自動で戻ることも、ここで確認できます

このレッスンのまとめ

  1. <form action={関数}>——preventDefault()不要、FormDataで受け取り、成功で自動リセット
  2. useActionState——[結果, action, 送信中]。自前のisSubmittingが要らなくなる
  3. useOptimistic——待たずに先に反映し、失敗したら自動で戻る
  4. 楽観的な更新はほぼ成功する操作だけ。決済のような場面では使わない
  5. useTransitionは優先順位を変える道具(useMemoは計算を減らす道具)
  6. useはフックのルールの外。ただし自前fetchの置きかえではない
  7. 既存の書き方もそのまま動く。読めるようにしておけば十分

次は、Next.jsです。ここで出てきたActionsは、そこでサーバー側とつながります。

やってみよう:フォームをactionで書きかえる

formsの回のフォームを、React 19の書き方で作り直します(元のファイルは残して、別名で作ってください)。

  1. src/components/AddBirdFormAction.tsxを作る
  2. useActionStateで、検証・保存・エラー返却を1つの関数にまとめる
  3. <form action={formAction}>にして、<input name="name">のようにname属性を付ける
  4. ボタンをdisabled={isPending}にし、文言を「追加中…」に切りかえる
  5. 一覧ページに一時的に置いて、送信中の表示成功後に入力欄が空になることを確認する
const [state, formAction, isPending] = useActionState(
  async (prevState, formData) => {
    const name = String(formData.get('name') ?? '').trim()
    if (name === '') return { error: '名前を入れてください' }

    await new Promise((resolve) => setTimeout(resolve, 1000))  // 送信のふり
    onAdd({ id: crypto.randomUUID(), name, area: 'ひみつ', emoji: '🐧', rare: 1, memo: '' })
    return { error: null }
  },
  { error: null },
)

5番で1秒間ボタンが「追加中…」になるのが見えるはずです。自前でisSubmittingを作らずにこれが出るのが、useActionStateの効きめです。

確認できたら、この確認用フォームは外してかまいません。どちらの書き方も現役なので、置きかえは急がなくて大丈夫です。

よくある質問

React 19の新機能は覚えないといけませんか?
既存の書き方も動くので急ぐ必要はありません。ただしフォームまわりは書く量がはっきり減りますし、新しい記事やライブラリのコードに出てくるので、読めるようにはしておくと困りません。
useActionStateは何をしてくれますか?
送信の処理・その結果・送信中かどうかの3つをまとめて持ちます。自前でisSubmittingやエラー用のstateを作る必要がなくなります。
useOptimisticはどんなときに使いますか?
サーバーの返事を待たずに、先に画面へ反映しておきたいときです。いいねボタンのように結果がほぼ成功する操作に向いていて、失敗したときはReactが自動で元に戻します。
useフックとuseEffectでのデータ取得は違いますか?
useはPromiseやContextを読むためのもので、条件分岐の中でも呼べる点が特別です。ただしデータ取得の入口としては、フレームワークやデータ取得ライブラリと組み合わせて使うのが前提になっています。