Next.jsを使っていて、フォームの入力状態とクエリパラメータを同期させたいことがあったので、どのように実現したかを紹介します。
その前に: router.push
やrouter.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とクエリパラメータを同期させる例
フォームの選択状態とクエリパラメータを同期させる例として、プルダウンの選択状態と同期させてみます。
こちらが完成形です。
パターン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を追加している点です。
このisInitialized
がtrue
になったら初期化処理は完了したとみなし、選択を切り替えるたびにvalue
をクエリパラメータに反映しています。
isInitialized
をtrue
にするタイミングは、router.isReady
がtrue
になった後にしています。
この前に初期化完了としてしまうと、router.query.param
がまだundefined
の状態でsetValue
を実行してしまうため、最初のクエリパラメータをフォームにうまく反映できません。
おまけ: Shallow Routingについて
router.push( // replaceの場合も同様
{
pathname: router.pathname,
query: {
param: newValue,
},
},
undefined,
{ shallow: true }
)
router.push
やrouter.replace
にはshallow
というオプションが存在します。
デフォルトはfalse
なのですが、これをtrue
にすると遷移時にページの読み込み直しが発生せず、ルートの変更だけおこなわれます。
具体的にはgetServerSideProps
、getStaticProps
、getInitialProps
などが呼び出されません。
これらを使っていない場合は挙動に違いは見られなかったのですが、今回のような要件であればルートの変更だけで十分なので、基本的にshallow: true
にしておくのがよさそうです。