JavaScriptでURLのクエリと向き合う

by 37108 at 2023/09/30

検索ページなどではクエリパラメータを取得してそれを検索条件に反映したり、リンクやナビゲーションにクエリパラメータを反映させることが多々あります。それ以外のページでも似たようなケースで扱うことが多々あります。 そのケースで便利なのがURLSearchParams というAPIになります。Next.js の AppRouter では useSearchParams APIを経由してこのインスタンスを取得するようになってたりします。

基本的なユースケース

URLSearchParams インスタンスを生成することができます。オプションとしてクエリパラメータを文字列、オブジェクト、配列などの形で渡すこともできます。

var p = new URLSearchParams();

var p = new URLSearchParams("q=greeting");
var p = new URLSearchParams({ q: "greeting" });
var p = new URLSearchParams(["q", "greeting"]);

MDN のドキュメントを参照すると、下記が引数として該当するのでMap などを同様に渡すことができたりもします。

const map = new Map();
map.set("q", "greeting");

const p = new URLSearchParams(map);

URLSearchParams を利用する

利用するといってもメソッドを呼び出すだけですね…。それらのメソッドもJSを触っていれば見慣れたようなものが多いのですんなり入ってくると思います。

まずインスタンスからクエリパラメータを文字列として出力してくれるメソッドです。

const p = new URLSearchParams("q=greeting");

console.log(p.toString());
// 'q=greeting'

qが複数ある場合のtoString の出力を見ていきます。q=greeting&q=foobar といった形になるため q[]=greeting&q[]=foobar などの形を予期している場合はこのまま利用できません。回避策としては URLSearchParams インスタンス作成時にキー自体を q[] といった形にするのが簡単かなぁと思います。

const p = new URLSearchParams([
  ["q", "greeting"],
  ["q", "foobar"],
]);

p.toString();
// 'q=greeting&q=foobar'

appenddeletesetなどの基本的な追加/削除の操作もあり、キーに該当する最初の値のみを返す、 get とキーに該当するすべての値を返す getAll というメソッドもあります。この辺りはとても直感的なインタフェースなので省略します。 Object.entries のように、クエリパラメータに含まれるすべてのキーと値をイテラブルに処理できる entries というメソッドもあります。またURLの正規化などに利用できる、ソートとして sort メソッドも備わっています。この辺りのメソッドはドキュメントを見れば一目瞭然かなと思ったりします。

ネストへの対応

ここからが本題というか、書きたかった部分なのですが、Next.js の Page Router でクエリパラメータを取得するには useRouter の返り値である router.query を利用するのが一番簡単なのですが、キーが同一のクエリパラメータが複数ある場合に返ってくるオブジェクトが下記のような指定になります。

// router.query
{
  q: ["greeting", "foobar"];
}

これをそのまま URLSearchParams のインスタンス生成に使うとネストされた値は考慮されないため下記のように解釈されます。

const p = new URLSearchParams({ q: ["greeting", "foobar"] });

p.toString();
// 'q=greeting%2Cfoobar'

ここで、先ほど示したように配列であれば問題なく解釈してくれることを利用してオブジェクトを配列に変換することで対応することができます。

const o = {
  q: ["greeting", "foobar"],
  r: "routing",
};

const v = [];
for (const [key, value] of Object.entries(o)) {
  if (typeof value === "string") {
    v.push([key, value]);
  } else {
    value.forEach((val) => {
      v.push([key, val]);
    });
  }
}

またクエリパラメータだけでなくURLの全体がわかる場合は URL インスタンスを経由してより簡単に作成することが可能です。 この辺りは実装時のユースケースに合わせて考えるのが良いかと思います。

const u = new URL("https://example.com?q=greeting&q=foobar&r=routing#ddd");
u.searchParams.toString();
// 'q=greeting&q=foobar&r=routing'

さいごに

URLSearchParams だけでなく、 URL や URLPattern といったAPIもあり(URLPatternは一部ブラウが未対応)、標準APIだけでもかなり便利な機能が使えるようになっています。 ぜひさまざま試してみると良いかと思いました。

参考