📚

こう変わった!App Router時代のフォーム開発

2024/12/18に公開

こんにちは!令和トラベル FrontendエンジニアのFukudaが、App Routerでのフォーム開発ついてご紹介します。ぜひ最後までご覧ください!

フォーム開発において、Next.jsのApp Routerは大きな可能性を広げました。特に、Server Actionsや新しい便利なフックの追加により、フォーム送信や状態管理、バリデーションが格段にシンプルで強力になっています。Pages Routerと比べて何が変わったのか、そしてどのように実装が進化したのか、実例を交えて見ていきましょう。

フォーム開発における新機能

1. Server Actions

Server Actionsを使用すると、非制御コンポーネントの利点を活かしつつ、フォーム送信をサーバーサイドで直接処理できます。

// app/form/actions.js
'use server'

export async function submitForm(formData) {
  // サーバーサイドでフォームデータを処理
  const result = await processFormData(formData);
  return result;
}

// app/form/page.jsx
import { submitForm } from './actions';

export default function Form() {
  return (
    <form action={submitForm}>
      <input name="email" type="email" />
      <button type="submit">送信</button>
    </form>
  );
}
制御コンポーネントと非制御コンポーネント
特徴 制御コンポーネント 非制御コンポーネント
状態管理 Reactの状態で管理 DOMで管理
値の取得 state経由 refまたはDOMから直接
リアルタイム処理 容易 困難
パフォーマンス 再レンダリングが多い 再レンダリングが少ない

制御コンポーネントの例:

const [email, setEmail] = useState('')
return (
  <input 
    name="email" 
    type="text" 
    value={email} 
    onChange={(e) => setEmail(e.target.value)}
  />
)

非制御コンポーネントの例:

const ref = useRef<HTMLInputElement>(null);
return (
  <input type="text" ref={ref} />
)

メリットとデメリット

制御コンポーネント:

  • メリット: 常に値にアクセスでき、リアクティブな実装が可能。
  • デメリット: 入力値の更新ごとに再レンダリングが発生し、パフォーマンスに影響を与える可能性がある。

非制御コンポーネント:

  • メリット: stateを経由しないため、入力値の更新ごとの再レンダリングが発生せず、パフォーマンスが向上。
  • デメリット: 入力中のバリデーションなど、リアルタイムな処理が難しい

2. 便利なHooks

useFormStateは、フォームアクションの結果に基づいて状態を更新するためのフックです。

'use client'
import { useFormState } from 'react-dom'
import { submitForm } from './actions'

const initialState = { message: '' }

export function Form() {
  const [state, formAction] = useFormState(submitForm, initialState)
  
  return (
    <form action={formAction}>
      {/* フォーム要素 */}
      <p>{state.message}</p>
      <button type="submit">送信</button>
    </form>
  )
}

useFormStatusは、フォームの送信状態を簡単に管理できる新機能です。

'use client'
import { useFormStatus } from 'react-dom'

export const SubmitButton = () => {
  const { pending } = useFormStatus()
  return (
    <button type="submit" disabled={pending}>
      {pending ? 'Sending...' : 'Send'}
    </button>
  )
}

3. サーバーコンポーネントとの統合

App Routerでは、フォームをサーバーコンポーネントとして実装できます。これにより、初期データの取得や表示が高速化され、SEOも向上します。

// app/form/page.jsx
export default async function FormPage() {
  const initialData = await fetchInitialData();
  return <Form initialData={initialData} />;
}

フォーム開発におけるPages RouterとApp Routerとの違い

フォーム開発における違いはいくつかあると思います。
ここでは、以下の2点でPages RouterとApp Routerを比較します。

  • フォーム処理
  • バリデーション

1. フォーム処理

Pages Router: クライアントでフォームを処理、適宜APIルートを呼び出す

// pages/api/submit.js
export default async function handler(req, res) {
  if (req.method === 'POST') {
    const { email, message } = req.body;
    // サーバーサイドでのデータ処理
    // ...
    res.status(200).json({ success: true });
  } else {
    res.status(405).json({ error: 'Method not allowed' });
  }
}

// pages/form.js
const handleSubmit = async (e) => {
  e.preventDefault();
  const res = await fetch('/api/submit', {
    method: 'POST',
    body: JSON.stringify({ email, message }),
    headers: { 'Content-Type': 'application/json' },
  });
  const data = await res.json();
  // レスポンス処理
};

App Router: Server Actionsにより、サーバーサイドで直接フォームを処理可能

// app/actions.js
'use server'

export async function submitForm(formData) {
  const email = formData.get('email');
  const message = formData.get('message');
  // サーバーサイドでのデータ処理
  // ...
  return { success: true };
}

// app/form/page.js
import { submitForm } from '../actions';

export default function Form() {
  return (
    <form action={submitForm}>
      <input name="email" type="email" />
      <textarea name="message"></textarea>
      <button type="submit">送信</button>
    </form>
  );
}

2. バリデーション

Pages Router: 主にクライアントサイドでバリデーションを行う

// pages/form.js
import { useState } from 'react';
import { z } from 'zod';

const schema = z.object({
  email: z.string().email(),
  message: z.string().min(10),
});

export default function Form() {
  const [errors, setErrors] = useState({});

  const handleSubmit = (e) => {
    e.preventDefault();
    const formData = new FormData(e.target);
    const result = schema.safeParse(Object.fromEntries(formData));
    if (!result.success) {
      setErrors(result.error.flatten().fieldErrors);
    } else {
      // フォーム送信処理
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      {/* フォーム要素 */}
      {errors.email && <p>{errors.email[0]}</p>}
      {errors.message && <p>{errors.message[0]}</p>}
    </form>
  );
}

App Router: Server Actionsでサーバーサイドでもバリデーションが可能

// app/actions.js
'use server'
import { z } from 'zod';

const schema = z.object({
  email: z.string().email(),
  message: z.string().min(10),
});

export async function submitForm(formData) {
  const result = schema.safeParse(Object.fromEntries(formData));
  if (!result.success) {
    return { errors: result.error.flatten().fieldErrors };
  }
  // バリデーション成功後の処理
  return { success: true };
}

// app/form/page.js
'use client'
import { useFormState } from 'react-dom';
import { submitForm } from '../actions';

export default function Form() {
  const [state, formAction] = useFormState(submitForm, {});

  return (
    <form action={formAction}>
      <input name="email" type="email" />
      {state.errors?.email && <p>{state.errors.email[0]}</p>}
      <textarea name="message"></textarea>
      {state.errors?.message && <p>{state.errors.message[0]}</p>}
      <button type="submit">送信</button>
    </form>
  );
}

バリデーション時に、use serverを注意して使わないとバリデーションJSコードが改変される危険性があるみたいなので注意が必要です。
👇詳しくは以下の記事で、わかりやすく説明されています。

https://zenn.dev/moozaru/articles/c7335f66dfb8df

まとめ

App Routerの導入により、サーバーサイドでの直接的な処理、パフォーマンスの向上など、生産性がとても向上しているなと感じています。
App Routerは一見複雑ですが、理解を深めるとフロントエンドにおける大きなソリューションを提供していけると思っており、フォームといった面でも多くの恩恵を受けれます。令和トラベルで開発しているNEWTでは、現状Pages Routerを使用していますが、App Routerを活用する動きは活発化しており、現状のフォームもどんどんApp Routerに順応する形に進んでいこうと思っています。フォーム開発の新時代に、ぜひ飛び込みましょう!

令和トラベルでは一緒に働く仲間を募集しています

この記事を読んで会社やプロダクトについて興味をお持ちいただけましたら、ご連絡お待ちしています!フランクに話だけでも聞きたいという方は、カジュアル面談も実施できますので、お気軽にお声がけください。

エンジニア採用 | 株式会社令和トラベル

令和トラベル Tech Blog

Discussion