react-router-dom は JavaScript のフレームワーク的ライブラリである React にルーティング機能を提供するライブラリである react-router の web ブラウザ用の機能群です。この中に Link というコンポーネントがあり、これが素の HTML における a タグ相当の動作を行ってくれます。更に NavLink という Link のラッパーコンポーネントが用意されており現在URLと関連付けしたスタイル付けを容易にしてくれます。
React – ユーザインターフェース構築のための JavaScript ライブラリ
React Router: Declarative Routing for React.js
React Router: Declarative Routing for React.js#web
この記事はそれぞれ以下のバージョンに対応する記事です。
- react: 17.0.2
- react-dom: 17.0.2
- react-router-dom: 5.3.0
Link や NavLink に限った話ではないのですが react-router は無用な再描画、再読み込みを防ぐように作られています。この目的のため、同じルーティング定義で同じコンポーネントに対してはページの移動命令を出しても移動しません。一応、ブラウザルーターに forceRefresh という props がありますがこれは JavaScript ファイルすら再読み込みする真の再読み込みであまり快適ではありません。
React Router: Declarative Routing for React.js#BrowserRouter/forcerefresh-bool
ここでは NavLink を例にしてリンク先が同じページでも強制再マウントする方法を紹介します。
通常の動作のデモが次です。
input1, input2 のいずれかのページを開き、input 中に文字列を入力し、再度同じページのリンクをクリックします。するとリンクをクリックしたのにも関わらず、ページが更新されません。無用な再描画、再読み込みを防ぐ方針としてはこれは正しいのですが、更新して欲しいパターンもあります。そういった際には次のようにします。
/** リンクコンポーネント */
const NavLinkForceRemount = (props) => {
const [routeUniqueKey, setRouteUniqueKey] = useState(`${Math.random()}`);
// 自分が to に文字列ばかり渡す人なのでこの形。関数やオブジェクトを積極的に使う場合は別途、書き換えが必要です
const to = {
pathname: typeof props.to === "string" ? props.to : "",
state: { routeUniqueKey }
};
return (
<NavLink
{...props}
onClick={() => setRouteUniqueKey(`${Math.random()}`)}
to={to}
>
{props.children}
</NavLink>
);
};
/**
* key の過剰切り替えを防ぐために用意した変数
* このファイルの中でのみ使用し、現在のURLというただ一つのものに紐づく
*/
let ROUTE_KEY = "";
/** ルーティングProps生成関数 */
const makeRouteComponentProps = (props) => {
// NavLink に
// to={{
// pathname: route.path,
// state: { routeUniqueKey },
// }}
// と書くと、この ROUTE_KEY 書き換え処理が走る
// 環境が許すならば
// props.location?.state?.routeUniqueKey || ROUTE_KEY;
// と書くのが楽
// props.location.state.routeUniqueKey
ROUTE_KEY =
(props.location.state && props.location.state.routeUniqueKey) || ROUTE_KEY;
return {
key: ROUTE_KEY,
...props
};
};
/** ルーティング定義部分 */
<Switch>
<Route
exact
path="/input1"
render={(routeProps) => <Input1st {...makeRouteComponentProps(routeProps)} />}
/>
<Route
exact
path="/input2"
render={(routeProps) => <Input2nd {...makeRouteComponentProps(routeProps)} />}
/>
</Switch>
今度は input1, input2 のいずれかのページを開き、input 中に文字列を入力し、強制再マウントのNavLink の側から再度同じページのリンクをクリックします。するとページが更新された様に動きます。更にこれは再マウントでとどまるため、ルーティングの外側で定義されたヘッダー、フッター等の不変の部分の再描画や、JavaScript そのものの再読み込みをする必要がなく高速な処理となっています。
これの実装では何をやっているかというと、NavLink をクリックした際にルーティング先のコンポーネントの key を書き換える処理を追加しています。
React はコンポーネント定義に加えて key によって同一のコンポーネントか否か、つまり再マウントの必要があるか否かを決定しています。
リストと key – React#key
このため NavLink によるページ移動に連動させてルーティング先の key を書き換えることができれば、強制的に再マウントを行う NavLink を作れます。この key の伝達をここでは次の流れで行っています。
- 上記ソースコード中の NavLinkForceRemount コンポーネントの中の routeUniqueKey
- react-router-dom の Route コンポ-ネントの props の内の render の引数 routeProps
- 上記ソースコード中の makeRouteComponentProps 関数の返り値の key プロパティ
- ルーティング先のコンポーネントの key
これにより NavLink からルーティング先のコンポーネントの key まで任意の値を伝えられ、key にユニークなランダムな値を送れば、必ず再マウントができる様になります。また react-router-dom から呼び出せる NavLink や Link そのものを使うなどして key を undefined として扱えば、従来通りの最小の再マウント、再読み込みの挙動も実現できます。