こう変わった!App Router時代のフォーム開発
こんにちは!令和トラベル 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コードが改変される危険性があるみたいなので注意が必要です。
👇詳しくは以下の記事で、わかりやすく説明されています。
まとめ
App Routerの導入により、サーバーサイドでの直接的な処理、パフォーマンスの向上など、生産性がとても向上しているなと感じています。
App Routerは一見複雑ですが、理解を深めるとフロントエンドにおける大きなソリューションを提供していけると思っており、フォームといった面でも多くの恩恵を受けれます。令和トラベルで開発しているNEWTでは、現状Pages Routerを使用していますが、App Routerを活用する動きは活発化しており、現状のフォームもどんどんApp Routerに順応する形に進んでいこうと思っています。フォーム開発の新時代に、ぜひ飛び込みましょう!
令和トラベルでは一緒に働く仲間を募集しています
この記事を読んで会社やプロダクトについて興味をお持ちいただけましたら、ご連絡お待ちしています!フランクに話だけでも聞きたいという方は、カジュアル面談も実施できますので、お気軽にお声がけください。
令和トラベルのTech Blogです。 「あたらしい旅行を、デザインする。」をミッションに、海外旅行におけるあたらしい体験や、あたらしい社会価値の提供を目指すデジタルトラベルエージェンシーです。海外ツアー・ホテル予約アプリ「NEWT(ニュート)」を提供しています。(NEWT:newt.net/)
Discussion