この記事は、「NEWT Product Advent Calendar 2024」Day11 および「React Advent Calendar 2024」Day18の記事となります。
こんにちは!「NEWT Product Advent Calendar 2024」13日目は、令和トラベル FrontendエンジニアのFukudaが、QAエンジニア Osuからバトンをもらい、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> ); }
details 制御コンポーネントと非制御コンポーネント
特徴 | 制御コンポーネント | 非制御コンポーネント |
状態管理 | 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に順応する形に進んでいこうと思っています。フォーム開発の新時代に、ぜひ飛び込みましょう!
「NEWT Tech Talk」のお知らせ
令和トラベルでは、このように技術的な知識や知見・成果を共有するLT会を毎月実施しています。発表テーマや令和トラベルに興味をお持ちいただいた方は、誰でも気軽に参加いただけます。
2025年1月のテーマは、 ” プロダクトマネジメント ”
新年第1弾のNEWT Tech Talkは、「NEWT(ニュート)」のプロダクト開発を牽引するPM 藤沼・Backendエンジニア兼PM 木村の2名が、”カスタマーファーストを実現するプロダクトマネジメントの舞台裏” と題して、LT形式で発表を行います。
そのほか、毎月開催している技術発信イベントについては、connpass にてメンバー登録して最新情報をお見逃しなく!
令和トラベルでは一緒に働く仲間を募集しています
この記事を読んで会社やプロダクトについて興味をお持ちいただけましたら、ご連絡お待ちしています!フランクに話だけでも聞きたいという方は、カジュアル面談も実施できますので、お気軽にお声がけください。
今年最後の特大セールも開催中!
NEWTでは、12/26 お昼11:59まで、2024年最後のおトクなセールを開催中です!
📣宣伝
最後までお読みいただき、ありがとうございました!
次回の「NEWT Product Advent Calendar 2024」Day14は、Backendエンジニアのnocciが担当します。「TypeScript Compiler APIを利用したデータベースドキュメントの自動生成」をテーマご紹介する予定です。次のブログもお楽しみに!