Next.jsで親子関係あるいはネストされたフォームを作る
Webアプリを開発していると、親子関係のモデル構造を、1つのフォームで一括で保存・更新する処理が頻繁に出てきますよね。例えばRailsでは accepts_nested_attributes_for
を使ってhas-manyなフォームが簡単に作れたりします。
これをNext.jsでも親子関係あるいはネストされたフォームを作る方法を紹介します。ただし、この記事ではUIの構築ではなく、構造化されたデータを受け取るための処理についてだけ解説をします。
フォームを構造化するには?
具体的なやり方を解説する前に、そもそもRailsやPHPではどのようにしてフォームデータを構造化しているのかを紹介します。以下は、Railsが出力するフォームのHTMLです。Userが親で、Articleが子の関係性で、ユーザが複数の記事を持っています。
<form action="/users" method="post">
<input type="text" name="user[name]">
<input type="text" name="user[articles_attributes][0][title]">
<input type="text" name="user[articles_attributes][1][title]">
</form>
注目してほしいのは name
属性です。 user[name]
これはUserモデルの名前フィールドを指すキーです。次に user[articles_attributes][0][title]
はUser配下のArticleモデルを配列として持ち、最初の記事の件名フィールドを指すキーになっています。
このように配列のインデックスや、PHPでいう連想配列のような記法を使ってフォームデータを構造化しています。
しかし、この仕様自体はHTMLやJavaScriptのFormDataAPIには全く関係がありません。user[articles_attributes][0][title]
と構造を表現しても、HTML的には単なる文字列のキーとして扱われます。
これをRailsでは、Rackがフォームデータをパースして構造化するのを自動でやってくれているだけです。また、PHPでも同様に言語レベルでPOSTデータを構造化して $_POST
で連想配列として扱えるようになっています。すごい便利ですね。
Node.jsの世界ではずっと使われているExpressも同様にパースすることができます。body-parser
や qs
というライブラリがパース部分を担当しています。
Next.jsでフォームを構造化する
Next.jsでも同じ様にフォームを構造化して親子関係のデータを良い感じに受け取ってみましょう。Next.js AppRouterではフォームにはServer Actionsを利用すると楽なので今回もServer Actionsを使ってフォームを作ります。
先程の例と同じように作ってみましょう。 articles_attributes
はちょっと長いので単純に articles
というキーに変更します。
import { submitAction } from "./submitAction";
export default function Page() {
return (
<>
<form action={submitAction}>
<input type="text" name="user[name]" />
<input type="text" name="user[articles][0][title]" />
<input type="text" name="user[articles][1][title]" />
<button type="submit">Submit</button>
</form>
</>
);
}
受け取ったFormDataをそのまま愚直に内容を出力してみます。
export async function submitAction(formData: FormData) {
console.log(Object.fromEntries(formData.entries()));
}
すると以下のような構造になっています。当然ですがやはり 'user[articles][0][title]'
は単なる文字列のキーになっていますね。
{
"user[name]": "1",
"user[articles][0][title]": "2",
"user[articles][1][title]": "3"
}
これをパースして以下のように受け取りたいです。
{
"user": {
"name":"1",
"articles":[
{"title":"2"},
{"title":"3"}
]
}
}
構造化させるためにはいくつか方法はありますが、今回は2つのライブラリを紹介します。
qsを使う
1つ目は定番ライブラリ qs
です。あらゆるパラメータをパースするのに昔から使われています。しかし、qsは直接FormDataを扱うことができません。
そこでqsがパースできるようにFormDataをURLSearchParamsを使って文字列に変換します。 user[name]=1&user[article][0][title]=2
のようなQueryString形式です。
import * as qs from "qs";
export async function submitAction(formData: FormData) {
const formDataToQueryString = (formData: FormData) => {
const params = new URLSearchParams();
for (const [key, value] of formData) {
params.append(key, value as string);
}
return params.toString();
};
const data = qs.parse(formDataToQueryString(formData));
}
こうすると構造化されたデータを受け取ることができます。
あとはZodやValibotのスキーマにこれをそのまま渡してあげることで、フォームデータを自前で編集せずにバリデーションをすることができて便利です。
const userSchema = z.object({
name: z.string().min(1),
articles: z.array(articleSchema),
});
const articleSchema = z.object({
title: z.string().min(1),
});
userSchema.safeParse(data);
parse-nested-form-dataを使う
比較的新しいライブラリでparse-nested-form-dataというものがあります。これはqsとは異なりFormDataAPIをそのままパースできるようになっています。
以下のように直接FormDataのオブジェクトを渡すだけで構造化されたデータを受け取れます。
import { parseFormData } from "parse-nested-form-data";
export async function submitAction(formData: FormData) {
const data = parseFormData(formData);
}
ただし、parse-nested-form-dataはRailsやPHPのような記法とはちょっと異なります。フィールド名は連想配列の []
ではなく、.
のオブジェクトのような記法ルールになっています。
- user[name] -> user.name
- user[article][0][title] -> user.article[0].title
好みになるかもしれませんが、個人的には直接FormDataを扱えるほうが楽なのでparse-nested-form-dataを使っています。
また、FormDataで受け取れる値はすべて文字列になってしまうのですが、parse-nested-form-dataには型変換もサポートしているのでとても便利です。例えば &isDelete=true
とキーの先頭に &
があるとBooleanとして値を受け取れます。
という感じで、Next.jsでもちょっと複雑な親子関係・ネストされたフォームを作って、良い感じにデータを受け取ることができるようになりました。もし他に良い方法があればコメントで教えて下さい!
Discussion
うわ、めっちゃ神記事です❤️❤️
ネストされたフォーム作りたかったんです😭