🔍

nuqs で Next.js のURLクエリパラメータの扱いを楽にする

2024/08/21に公開

こんにちは 👋

株式会社 Rehab for JAPAN フロントエンドエンジニアの okazawa です!

フロントエンド開発をしていると、タブの選択状態やページネーション等、URL のクエリパラメータで画面の状態を管理したい場面が多々あります。ただ、クエリパラメータの管理って結構面倒ですよね。特に複雑な画面になってくると特に...😫

そこで今回は、Next.js 用のクエリパラメータ用状態管理ライブラリである nuqs を使ってクエリパラメータの管理を楽にしてみたいと思います!

ちなみに、nuqsは元々next-usequerystateという名前のライブラリで、名前が長くて入力するのが面倒という経緯から略称の nuqs になったらしいです(公式サイトより

作りたいもの

今回は nuqs のオーソドックスな使い方を見て、「へぇ〜、こんなのがあるんだ」と感じてもらえれば良いので、今回は以下のような画面を作りたいと思います 🧑‍💻

  • 技術スタック

    • Next.js(App Router) v14.2.5
    • Chakra UI v2.8.2
    • nuqs v1.17.7
  • 画面要件

    • 画面に複数タブがある
    • 画面をリロードしたときにタブの選択状態を保持する
    • ブラウザバックしたときに、前回選択していたタブを選択状態にする

とりあえず nuqs を使わずに作ってみる

「nuqs 使うとこんな感じになりますよ〜」ということをより分かりやすくするために、まずは面倒ですが nuqs を使わずに画面要件を満たす実装をしてみたいと思います 😓

not-nuqs/page.tsx
"use client";

import {
  Button,
  Grid,
  Tab,
  TabList,
  TabPanel,
  TabPanels,
  Tabs,
  Text,
} from "@chakra-ui/react";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { useEffect, useState } from "react";

export default function NextRouter() {
  const router = useRouter();
  const searchParams = useSearchParams();
  const pathname = usePathname();

  const [index, setIndex] = useState(Number(searchParams.get("index")) || 0);

  useEffect(() => {
    setIndex(Number(searchParams.get("index")) || 0);
  }, [searchParams]);

  const onTabClick = (index: number) => {
    const params = new URLSearchParams(searchParams);
    params.set("index", index.toString());
    router.push(`${pathname}?${params}`);
  };

  const onButtonClick = () => {
    router.push("/");
  };

  return (
    <Grid gridTemplateRows={"1fr auto"}>
      <Tabs index={index} size="md" variant="enclosed" borderColor={"blue.500"}>
        <TabList>
          <Tab onClick={() => onTabClick(0)}>Tab 0</Tab>
          <Tab onClick={() => onTabClick(1)}>Tab 1</Tab>
          <Tab onClick={() => onTabClick(2)}>Tab 2</Tab>
        </TabList>
        <TabPanels>
          <TabPanel>
            <Text fontSize={"xl"}>たぶぜろ</Text>
          </TabPanel>
          <TabPanel>
            <Text fontSize={"xl"}>たぶいち</Text>
          </TabPanel>
          <TabPanel>
            <Text fontSize={"xl"}>たぶに</Text>
          </TabPanel>
        </TabPanels>
      </Tabs>
      <Button variant={"link"} onClick={onButtonClick}>
        TOPに戻る
      </Button>
    </Grid>
  );
}

onTabClick内の処理でrouter.push()することでクエリパラメータの変更と履歴スタックへの追加をして、searchParamsの変更をuseEffect()で検知することでタブの選択状態が変わる感じです。

出来上がった画面はこんな感じです!

nuqsを使わないパターンの実装画面のデモ

見にくいですが、タブをクリックすると URL のindex=の値が変わって、画面リロードやブラウザバックを行ったときにタブの選択状態が画面要件通りになっていることが確認できます。

nuqs を使って実装してみる

いよいよ nuqs を使ってみたいと思います。

"use client";

import {
  Button,
  Grid,
  Tab,
  TabList,
  TabPanel,
  TabPanels,
  Tabs,
  Text,
} from "@chakra-ui/react";
import { useRouter } from "next/navigation";
import { parseAsInteger, useQueryState } from "nuqs";

export default function NextRouter() {
  const router = useRouter();
  const [index, setIndex] = useQueryState(
    "index",
    parseAsInteger
      .withDefault(0)
      .withOptions({ history: "push", scroll: false })
  );

  const onTabClick = (index: number) => {
    setIndex(index);
  };

  const onButtonClick = () => {
    router.push("/");
  };

  return (
    <Grid gridTemplateRows={"1fr auto"}>
      <Tabs index={index} size="md" variant="enclosed" borderColor={"blue.500"}>
        <TabList>
          <Tab onClick={() => onTabClick(0)}>Tab 0</Tab>
          <Tab onClick={() => onTabClick(1)}>Tab 1</Tab>
          <Tab onClick={() => onTabClick(2)}>Tab 2</Tab>
        </TabList>
        <TabPanels>
          <TabPanel>
            <Text fontSize={"xl"}>たぶぜろ</Text>
          </TabPanel>
          <TabPanel>
            <Text fontSize={"xl"}>たぶいち</Text>
          </TabPanel>
          <TabPanel>
            <Text fontSize={"xl"}>たぶに</Text>
          </TabPanel>
        </TabPanels>
      </Tabs>
      <Button colorScheme="black" variant={"link"} onClick={onButtonClick}>
        TOPに戻る
      </Button>
    </Grid>
  );
}

注目すべきはuseQueryStateの部分で、簡単に説明すると、「indexというクエリパラメータの状態管理をしますよ」ということを宣言している感じですね。

詳しく見ると

const [index, setIndex] = useQueryState(
  "index",
  parseAsInteger // デフォルトだとstring型で取得されるクエリパラメータを整数にパースする
    .withDefault(0) // クエリパラメータに値がなかった場合にデフォルトで指定される値の設定
    .withOptions({ history: "push", scroll: false }) // Next.js の useRouter() で router.push 等をするときに指定できるようなオプションの指定
);

という感じです。Parser は種類が豊富でDocumentに丁寧に説明されています。特に今回の例だと、tab の index は number 型で指定する必要があるので、毎回毎回 string 型 → number 型へ変換しなくても良いことになります 👍

そして、onTabClickの処理は

const onTabClick = (index: number) => {
  setIndex(index);
};

なんとこんなにスッキリ書けてしまいます!
setIndex(index)の実行だけで、クエリパラメータの更新から履歴スタックへの追加まで全て行ってくれます!

とてもシンプルでコードの見通しも結構良くなったかなと思います ✨

画面の動きを見ても、しっかり画面要件を満たせています 🙌

nuqsを使うパターンの実装画面のデモ

まとめ

nuqs を使うメリットとしては

  • クエリパラメータの状態管理がシンプルになる
  • useQueryStateの例のように提供されている API が直感的に使いやすい
  • デフォルト値の設定や Parser のおかげで予期しないエラーを防ぎやすい

といったことが挙げられると思います。また、今回の記事の執筆にあたって調べる中で nuqs の良いなと思った点は

  • 公式 Document が分かりやすい
  • Pages Router と App Router の両方に対応している
  • テストがしっかり行われているので安心して使える

で、学習コストやメンテの面から見ても導入のハードルは比較的低いのかなと感じています!

Next.js を採用していて複雑なクエリパラメータ管理に悩んでいる場合は導入を検討してみてはいかがでしょうか?

以上で nuqs の紹介を終わります。最後までお読みいただきありがとうございました 👋

今回作ったもの

デモ用のサイト

https://nuqs-demo.vercel.app

リポジトリ

https://github.com/okazawa0929/nuqs-demo

Rehab Tech Blog

Discussion