📩

Next.js(App Router)のformまわり

2024/06/06に公開

はじめに

今更ですが、App Router の登場で、フォーム周りの体験が今までから大きく変化し、よりパフォーマンス向上やセキュアにフォーム実装することが可能になりました。その辺り、備忘録兼ねてまとめます。

仕様

  • next 14.0.4 (App Router)
    • 14 系から Server Actions が stable になっています。
  • zod 3.22.4
    • フォームの validation に使っています。

ユースケース

実際に使用しているコードはもっと色々枝葉がついていますが、簡略化しています。
このページでは、単純にタイトルだけを送信する form を提供しています。

  • 送信する form で全体をラッピング
  • タイトルを入力する input
  • 提出するボタン

この 3 つが大きな構成要素です。

本体

index.tsx
import { submitCreate } from './submit.ts'

// Server Component
export const SomePage = async () => {
  return (
    <form
      id="create-content"
      action={submitCreate}
      className="flex flex-col gap-16"
    >
      <Input
        name="title"
        placeholder="タイトルが入ります"
        type="string"
      />
      <ButtonSubmit />
    </form>
  )
}

実行される処理

submit.ts
'use server'

export const submitCreate = async (formData: FormData) => {
  const formJson = formDataToJSON(formData)
  const result = SCHEMA.safeParse(formJson)
  if (!result.success) {
    // validationに失敗した時の処理
  } else {
    // validationをクリアした後の処理
  }
}

Server Actions の登場により、サーバー側で処理を行う非同期関数を formaction に指定することができるようになりました。
従来であれば、API Routes で別途 API を作って fetch する形だったのが、このように直接 action を使って実行することができるようになったので、API をわざわざ作成する必要がなくなり便利になりました。
formでラップされnameで指定された要素の値がformDataに入ります。SCHEMAは zod のスキーマです。

送信ボタン

components.tsx
'use client'

import { useFormStatus } from 'react-dom'
import { LuLoader2 } from 'react-icons/lu'
import { Button } from '@/components/ui/button'

// Client Component
export const ButtonSubmit = () => {
  const { pending } = useFormStatus()

  return (
    <Button
      type="submit"
      form="create-content"
      disabled={pending}
    >
      {pending && (
        <LuLoader2 className="h-4 w-4 animate-spin" /> // ローディングのアニメーション
      )}
      {pending ? '' : '送信する'}
    </Button>
  )
}

Server Component(SC)を使うことで様々な恩恵が受けられますが、できないこともあります
そのうちのひとつで大きなものが hooks(いわゆる use がつくものたち)が使えない点です。
React には複数の便利な hooks が用意されていますが、それが使えるのは Client Component(CC)のみです。
(むやみやたらに hooks を使ってカオスな状態管理や無駄な副作用の増加を抑制するというメリットととも取れますが)

すなわち、リアクティブな状態管理を前提とするような機能、ここでいうローディングのような機能も、SC のみでは実装することは困難です。
そこで、上記のように ButtonSubmit という CC を使ってボタンを作ります。
こうすることで、ボタンのリアクティブな動作は担保しつつ、処理自体はサーバー側に委ねることができます。

そして、Next.js の公式ドキュメントでもユースケースが紹介されていますが、useFormStatus を使うのが最も利便性が高いです。useFormStatusform の状態を返してくれるので、この hooks を使ってローディングを表現することができます。

あれこれ

以上が App Router のフォームの実装の基本であり、おおよそこの組み合わせでフォームは作成することができますが、ちょっと余談を。

限界まで SC を使う

勘違いされがちですが、Server Actions は CC でも使えます。故に、わざわざ上記のように SC と CC を組み合わせずとも、Server Actions を使えば、fetch 処理はサーバー側で行われます。

index.tsx
'use client'

import { submitCreate } from './submit.ts'

// Client Component
export const SomePage = async () => {
  const { pending } = useFormStatus() // CCなのでhooksが使える

  return (
    <form
      id="create-content"
      action={submitCreate} // submitCreateはServer Actionsなのでサーバー側で実行される
      className="flex flex-col gap-16"
    >
      <Input
        name="title"
        placeholder="タイトルが入ります"
        type="string"
      />
      <Button
        type="submit"
        form="create-content"
        disabled={pending}
      >
        {pending && (
          <LuLoader2 className="h-4 w-4 animate-spin" /> // ローディングのアニメーション
        )}
        {pending ? '' : '送信する'}
      </Button>
    </form>
  )
}

このような単純な例であればこれで十分ですが、form の中では様々な実装を行います。例えば fetch が必要な選択肢を有するセレクトボックスだったり。なので、基本、App Router を使っている以上は、限界まで SC を使うのが App Router で実装する上では恩恵を受けられるのではと考えてそうしています。それが例えコンポーネントを増やすことになっても。

React Canary

React Canaryとは、React が開発している新機能を先んじて他のフレームワーク等で使えるようにするためのプロジェクト?です。

上記の Server Actions も useFormStatus フックも、React としてはまだ安定版ではないですが、Next.js においては安定提供されている機能になります。

React 19 の beta 版もこの 4 月にリリースされ、さらに先日、Next.js 15 の RC(Release Candidate)も発表されていました。またそのあたりも勉強・研究していかねば 😇

プラスクラス・スポーツ・インキュベーション株式会社

Discussion