FormDataによるフォーム送信 第1回 FormDataの基本とその特徴

フロントエンド開発においてはJSONでのフォーム送信が主流でしたが、最近ではFormDataを使う構成が注目されています。この記事では、FormDataの基本とその特徴を解説します。

発行

著者 藤田 智朗 フロントエンド・エンジニア
FormDataによるフォーム送信 シリーズの記事一覧

はじめに

フロントエンド開発において、長らく「フォーム送信」といえばJSONとして送信するというスタイルが定番になっていたのではないでしょうか?

筆者の推測にはなりますが、これにはクライアントアプリケーションとWeb APIとのやり取りに使うデータ形式として、JSONを使う方式が主流になったことが関係していると思われます。

また、Reactなどのコンポーネント指向ライブラリの普及も影響していると考えられます。useStateなどのステート管理APIでフォーム値を管理する際、オブジェクト形式にすることで扱いやすくなり、さらにオブジェクトはJSONへの変換が容易で、フォームの状態管理と送信データの形式が一致しやすいという利点があります。

たとえば、次のようにHTTPヘッダーに"Content-Type": "application/json"を指定し、送信内容はJSON.stringifyでJSON文字列にするといった具合です。

フォームの値をuseStateで管理し、JSONで送信するReactコンポーネントの例

const [title, setTitle] = useState("");
const [content, setContent] = useState("");

const handleSubmit = async (event) => {
  event.preventDefault();
  await fetch("/api", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ title, content }),
  });
};

return (
  <form onSubmit={handleSubmit}>
    <div>
      <label htmlFor="title">タイトル</label>
      <input
        type="text"
        id="title"
        value={title}
        onChange={(e) => setTitle(e.target.value)}
        required
      />
    </div>
    <div>
      <label htmlFor="content">内容</label>
      <textarea
        id="content"
        value={content}
        onChange={(e) => setContent(e.target.value)}
        required
      />
    </div>
    <button type="submit">送信</button>
  </form>
);

実際の現場ではreact-hook-formやそれに類するフォームの状態管理ライブラリを使うことで、直接このようなコードを書くことはないと思いますが、内部で同様のことをしているものが多いでしょう。

こういったライブラリを使うことでフォームの状態管理を制御しやすくなる一方で、フォームのマークアップ部分がライブラリに依存してしまうといった課題があります。

そのためか、最近はそういったライブラリによる依存を軽減し、本来のフォームのシンプルな実装を目指す動きがあるように筆者は感じています。

そこで使用されているのが、昔から存在するブラウザ標準のAPIであるFormDataです。

たとえば、Next.jsやAstroの公式サイトでは、FormDataを使ったサンプルコードが掲載されています。

どちらも<form>タグで囲まれるフォームのマークアップはシンプルな構成となっており、フォーム送信を受け取るサーバー側(Next.jsではサーバーアクション、AstroではAPIエンドポイント)でも値をFormDataとして処理しており簡潔なコードになっています。

FormDataは、HTMLフォームのデータを簡単に取得・操作・送信するためのブラウザ標準のAPIで、非常に歴史のあるAPIです。

今回の記事では、この古くて新しいFormDataの詳細について解説します。

FormDataの基本

この章では、FormDataの基本構文と主要な使い方を確認していきます。

FormDataオブジェクトの生成

FormDataを扱うには、まずnew FormData()によってFormDataオブジェクトを生成することから始めます。FormDataオブジェクトの生成方法は、主に「既存のフォームを引数に生成する場合」と「引数なしで生成する場合」の2つがあります。

まず、既存のフォームを引数に生成する場合を見てみましょう。

既存のフォームを引数に生成する場合

const form = document.querySelector("form");
const formData = new FormData(form);

既存のフォームを引数に生成する場合、formDataにはフォーム内の<input>などのフォーム要素に基づいたキーと値のペアのデータが作成されます。

たとえば、フォームに下記のようなinput要素があった場合、formDataにはname: "太郎"age: "20"のようなキーと値のペアのデータが作成されます。

キーと値のペアのデータが作成される

<input type="text" name="name" value="太郎">
<input type="text" name="age" value="20">

次に、引数なしで生成する場合のFormDataオブジェクトの生成方法です。

引数なしで生成する場合

const formData = new FormData();

引数なしで生成する場合は、formDataが生成されるものの、キーと値のデータが何も入っていない状態になります。

formDataにはappendメソッドを使ってあとからデータを登録することも可能です。

あとからデータを登録する

formData.append("name", "太郎");

FormDataのメソッド

append以外にも、FormDataにはデータを管理するためのメソッドが用意されています。基本的なメソッドについて以下に紹介しておきます。サンプルコードは、先ほどのname: "太郎"age: "20"のキーと値のペアのデータが作成されているものとしてください。

formData.get(key)は指定したキーの値を取得します。詳細は後述しますが、同じキーが複数ある場合は1つ目の値を取得します。

console.log(formData.get("name")) // 太郎を出力
console.log(formData.get("age")) // 20を出力

formData.has(key)は指定したキーが存在するか否かの真偽値を返します。

console.log(formData.has("name")) // trueを出力
console.log(formData.has("nameKana")) // falseを出力

formData.keys()はキーの、formData.values()は値の、formData.entries()はキーと値のペアのイテレータを返します。

for (const key of formData.keys()) {
  console.log(key); // "name" "age" を順に出力
}
for (const value of formData.values()) {
  console.log(value); // "太郎" "20" を順に出力
}
for (const keyValue of formData.entries()) {
  console.log(keyValue); // ["name", "太郎"] ["age", "20"] を順に出力
}

FormDataの特徴と注意点

FormDataの基本構文と主要な使い方を確認したところで、次のようなFormDataの特徴と注意点について解説します。

  • FormDataはイテラブルなオブジェクトの特徴を持つ
  • オブジェクトリテラルとしては扱えない
  • 同じキーのデータを複数持つことが可能

一つずつ、詳しく見ていきましょう。

FormDataはイテラブルの特徴を持つ

FormDataはキーと値のペアのデータを持つため、オブジェクトリテラル({})のように扱えると思われるかもしれませんが、同じブラウザ標準のAPIであるMapURLSearchParamsなどと同じ「イテラブル」の特徴を持っています。

イテラブルなオブジェクトは、Symbol.iteratorプロパティが実装されている、for-ofから順番に取り出した値を処理できる、といった特徴があります。

それを確認してみましょう。下記はFormDataがイテラブルであることを示すコードになります。

FormDataはイテラブルであることを確認する

const formData = new FormData(form);

// 1. Symbol.iterator を持っているか確認
console.log(typeof formData[Symbol.iterator]); // functionを出力

// 2. for-ofでループできるか確認
for (const keyValue of formData) {
  console.log(keyValue); // ["name", "太郎"] ["age", "20"] を順に出力
}
// entries() を付けた場合も同じ結果になる
for (const keyValue of formData.entries()) {
  console.log(keyValue); // ["name", "太郎"] ["age", "20"] を順に出力
}

イテラブルの詳細については「ECMAScript 2015の新機能 | 最終回 Iteration」を参照してください。

オブジェクトリテラルとしては扱えない

FormDataのもう一つの特徴として、オブジェクトリテラルではないため、Object.keys()JSON.stringify()で扱えないことに注意してください。

FormDataはオブジェクトリテラルのように扱えない

const formData = new FormData();
formData.append("count", 123);
formData.append("isSelected", true);

// データが正しく取得できない
console.log(Object.keys(formData)); // [] が出力
console.log(JSON.stringify(formData)); // "{}" を出力

オブジェクトリテラルとして扱いたい場合は、Object.fromEntries()を使うと便利です。

オブジェクトリテラルに変換

const obj = Object.fromEntries(formData.entries());
console.log(obj); // { count: "123", isSelected: "true" } を出力

同じキーのデータを複数持つことが可能

FormDataのキーは同じキーのデータを複数持つことが可能です。これはFormDataがHTMLのフォームに基づいていることが要因であると考えられます。

たとえばチェックボックスを配置する場合、同じname属性の値を持つ<input>要素を配置することが一般的かと思います。

同じname属性の値を持つinput要素を配置

<input type="checkbox" name="skills" value="html" checked />
<input type="checkbox" name="skills" value="css" checked />

この場合、formData.get()ではチェックされた要素の1つ目の値のみ取得します。formData.getAll()を使用すれば、チェックされた要素のすべての値を配列で取得します。

get()とgetAll()の使い方

console.log(formData.get("skills")); // "html"を出力
console.log(formData.getAll("skills")); // ["html", "css"]を出力

こういった事情があるため、キーと値のペアのデータを設定するには、最初に紹介したappend()の他にset()メソッドが用意されています。append()はすでに存在するキーを指定すると同じキーのデータが作成されるのに対して、set()は同じキーがあればそのデータを上書きします。

set()の使い方

formData.set("skills", "javascript");
console.log(formData.getAll("skills")); // ["javascript"]を出力

ここまでのまとめ

今回はFormDataの基本的な使い方や特徴、JSONとの送信形式の違いについて解説しました。

次回は今回の説明を踏まえたうえで、実際にFormDataを使ったフォーム送信の実装例を紹介しつつ、FormDataによって値がどのように送信されるのかも詳しく解説したいと思います。