LoginSignup
2
2

More than 1 year has passed since last update.

【Next.js x React】stateとクエリパラメータを同期させる方法

Posted at

Next.jsを使っていて、フォームの入力状態とクエリパラメータを同期させたいことがあったので、どのように実現したかを紹介します。

その前に: router.pushrouter.replaceによる遷移のきほん

とあるページ上でボタンをクリックしたときに画面遷移させる場合、

import { useRouter } from "next/router";

const Page = () => {
  const router = useRouter();

  return <button onClick={() => router.push("/path")}>click</button>; // /pathに遷移する
};

export default Page;

のようにルーターのpushメソッドにパスを渡して使いますよね?

このpushメソッドはパスと一緒にクエリパラメータも渡すことができます。

import { useRouter } from "next/router";

const Page = () => {
  const router = useRouter();

  return (
    <button
      onClick={() => {
        router.push({
          pathname: "/path",
          query: { param: 1 },  // /path?param=1 に遷移する
        });
      }}
    >
      click
    </button>
  );
};

export default Page;

これを利用することで、同じパスでクエリパラメータだけ更新することができます。

ちなみにpushメソッドの代わりにreplaceメソッドを使うとhistory(画面遷移の履歴)に新しく遷移先を追加するのではなく、現在の画面の履歴を置き換えられます。
例えば、[画面1] -push-> [画面2] -replace-> [画面3]と遷移してブラウザの戻るボタンを押すと画面2ではなく画面1に戻るようになります。

本題: stateとクエリパラメータを同期させる例

フォームの選択状態とクエリパラメータを同期させる例として、プルダウンの選択状態と同期させてみます。
こちらが完成形です。
pattern2.gif

パターン1: router.query.paramをそのまま使う場合

プルダウンの選択状態を管理するstateとしてrouter.query.paramをのまま使う場合は次のようなコードで実現できます。

import { useRouter } from "next/router";

const Page = () => {
  const router = useRouter();

  return (
    <select
      value={router.query.param}
      onChange={(e) => {
        router.push({
          // 選択変更のたびにhistoryには保存する。保存したくない場合はreplaceを使う
          pathname: router.pathname,
          query: {
            param: e.currentTarget.value,
          },
        });
      }}
    >
      {["1", "2", "3"].map((n) => (
        <option value={n} key={n}>
          {n}
        </option>
      ))}
    </select>
  );
};

export default Page;

選択肢のvalueにはrouter.query.paramを直接使用していて、選択肢を切り替えるたびにrouter.pushでクエリパラメータを更新しています。シンプルですね。

パターン2: 別のstateと同期させる場合

react-hook-formなどのライブラリを使っているとフォームの状態は別のstateで管理する必要があるかもしれません。
その場合は次のようにuseEffectを使って連動させることができます。

import { useRouter } from "next/router";
import { useEffect, useState } from "react";

const Page = () => {
  const router = useRouter();
  const param = router.query.param;
  const [value, setValue] = useState("1"); // 選択の状態を管理する。初期状態は1を指定
  const [isInitialized, setIsInitialized] = useState(false); // 最初のページ遷移直後のクエリパラメータ反映完了フラグ

  // ページ遷移直後にクエリパラメータ -> stateの反映をする
  // router.isReadyがtrueになるのを待つことでrouter.queryに値が入ってから反映できる
  // ブラウザの戻るボタンなどでクエリパラメータが変化した場合もフォームに反映されるようにrouter.query.paramもuseEffectの依存配列に加えている
  useEffect(() => {
    if (!router.isReady) {
      return;
    }
    param && setValue(param);
    setIsInitialized(true);
  }, [router.isReady, param]);

  // 選択が切り替わったときにstate -> クエリパラメータの反映をする
  useEffect(() => {
    // 最初のページ遷移直後のクエリパラメータ反映完了前はクエリパラメータを書き換えないようにする
    if (!isInitialized) {
      return;
    }
    router.push({
      pathname: router.pathname,
      query: {
        param: value,
      },
    });
  }, [isInitialized, value]);

  return (
    <select
      value={value}
      onChange={(e) => {
        setValue(e.currentTarget.value);
      }}
    >
      {["1", "2", "3"].map((n) => (
        <option value={n} key={n}>
          {n}
        </option>
      ))}
    </select>
  );
};

export default Page;

パターン1に比べて若干ややこしくなりましたね。

ポイントは、最初のページ遷移直後にクエリパラメータをstateに反映する前に、フォームの初期値がクエリパラメータを上書きしてしまうのを避けるため、isInitializedというフラグのstateを追加している点です。
このisInitializedtrueになったら初期化処理は完了したとみなし、選択を切り替えるたびにvalueをクエリパラメータに反映しています。
isInitializedtrueにするタイミングは、router.isReadytrueになった後にしています。
この前に初期化完了としてしまうと、router.query.paramがまだundefinedの状態でsetValueを実行してしまうため、最初のクエリパラメータをフォームにうまく反映できません。

おまけ: Shallow Routingについて

router.push(  // replaceの場合も同様
  {
    pathname: router.pathname,
    query: {
      param: newValue,
    },
  },
  undefined,
  { shallow: true }
)

router.pushrouter.replaceにはshallowというオプションが存在します。
デフォルトはfalseなのですが、これをtrueにすると遷移時にページの読み込み直しが発生せず、ルートの変更だけおこなわれます。

具体的にはgetServerSidePropsgetStaticPropsgetInitialPropsなどが呼び出されません。
これらを使っていない場合は挙動に違いは見られなかったのですが、今回のような要件であればルートの変更だけで十分なので、基本的にshallow: trueにしておくのがよさそうです。

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2