react-routerで現在のlocationを取得する2種類の方法の使い分け方
2020年6月10日 公開 / 2020年6月10日 更新
SPAを作る際は、URLを変化させたり、URLの変化に反応して画面を変えたりする必要があります。このために使われるのがルーティングライブラリです。Reactにおいては、react-routerが代表格として知られています。
react-routerでルーティングが制御されている場合、その中のコンポーネントが現在のURLを表すオブジェクトであるlocationを得るための方法は大別して2つあります。一つはuseLocation
、もう一つはuseHistory
です。なお、これらのフックはreact-routerのv5.1で追加されました。この記事ではこれ以前の方法は取り扱いません。
この2つの方法のどちらを使ってもlocation
を得ることは可能ですが、どちらを使うべきかは場合によって明確に異なります。間違った方を使うと、パフォーマンスが低下したり期待通りに動かなかったりという問題が発生することになります。
useLocation
とuseHistory
の使い方
さて、useLocation
はその名の通り現在のlocation
を返すフックです。例えば次のようなコンポーネントがあれば、常に現在のURL(パス)を表示し続けるでしょう。
const ShowLocation = ()=> {
const location = useLocation();
return <div>
{location.pathname}
</div>;
}
一方、useHistory
はhistory
オブジェクトを返します。このオブジェクトはURLを変えたいときに使えるhistory.push
といったメソッドを提供しています。例えば、ボタンがクリックされたときに別のURLに移動したい場合は次のような実装ができます。
const NextPageButton = ()=> {
const history = useHistory();
return <button onClick={()=> {
history.push('/some/url');
}}>click me!</button>;
}
これらがuseLocation
とuseHistory
の基本的な使い方です。この記事で特に注目したいのは、history.location
とすることでhistory
オブジェクトのプロパティから前述のlocation
オブジェクトを取得できるという点です。
これを使うと、ShowLocation
は次のように書き換えられるように一見思われます。
const ShowLocation = ()=> {
const history = useHistory();
return <div>
{history.location.pathname}
</div>;
}
お察しの通り、この実装はうまく動きません。実は以前はこの実装でも動きましたが、react-routerの5.2.0(2020年5月12日リリース)からはうまく動かなくなりました。うまく動かないというのは、URLが変わってもShowLocation
が再レンダリングされず、常に現在のURLを表示することができないということです。
history
は常に同じオブジェクトである
ここでの根本的な問題は、location
はURLが変わると新しいオブジェクトが作られるのに対して、history
は常に同じオブジェクトであるということです。すなわち、URLが変わってもuseHistory
から返されるhistory
は常に同じオブジェクトです1。ただし、location
がイミュータブルなものである代わりに、history
はミュータブルです(ユーザーが勝手にhistory
を書き換えることはありませんが)。
具体的には、ページ遷移の際にはURLが変わるので新しいlocation
オブジェクトが作られる一方、history
は同じオブジェクトが引き続き使用されます。このとき、history.location
は再代入によって書き換えられます。これにより、history
は同じオブジェクトだが、history.location
を参照すると常に最新のURLが取得できることになります。
ReactのuseState
フックやuseContext
フックは、その結果が変化したときにコンポーネントを再レンダリングします。そうなると、useLocation
の返り値はURLが変わるたびに新しいオブジェクトになるため、URLが変わると再レンダリングが発生するのは自然ですね。一方で、history
オブジェクトは常に同じであるため、useHistory
の返り値は常に同じオブジェクトです。ならば、URLが変わってもuseHistory
は再レンダリングを発生させないのが妥当に思えます。
特に、Reactにおいて「オブジェクトが常に同じかどうか」という観点はパフォーマンスを考慮すると重要です。実際、useState
のステート更新関数やuseReducer
のdispatch
関数などは常に同じであることがAPIリファレンスにで明示されています。
従来(5.2.0より前)のreact-router
では、URLが変わった際に、結果が変わらないのにuseHistory
による再レンダリングが発生していました。5.2.0ではこれが発生しないように改善されたのです。
useLocation
とuseHistory
の使い分け方
ここまででuseLocation
とuseHistory
の違いが分かりましたね。前者はURLが変わると再レンダリングが発生して新しいlocation
オブジェクトが得られる一方で、後者の結果であるhistory
オブジェクトは常に同じであり、そのためuseHistory
はURLが変わっても再レンダリングを起こしません。
ここから言えることは、レンダリング中にlocation
が必要ならばuseLocation
を使い、そうでないならuseHistory
を使うべきであるということです。
先ほどのShowLocation
はレンダリング中にlocation
が必要な例です。実際、レンダリング結果にlocation
が影響しています。このような場合、location
の変化に追随して再レンダリングを行う必要がありますから、useLocation
を使う必要があります。
const ShowLocation = ()=> {
const location = useLocation();
return <div>
{location.pathname}
</div>;
}
一方で、次のコンポーネントを考えてみましょう。
const ShowLocationButton = ()=> {
const location = useLocation();
const onClick = ()=> {
alert(location.pathname);
}
return <button onClick={onClick}>
show location
</div>;
}
このボタンをクリックすると、現在のURLがアラートで表示されます。こちらの例ではlocation
はonClick
関数の中でのみ使用されています。つまり、実際にレンダリングされる瞬間にlocation
は使われないということです。このような場合はuseHistory
で代替できます。
const ShowLocationButton = ()=> {
const history = useHistory();
const onClick = ()=> {
alert(history.location.pathname);
}
return <button onClick={onClick}>
show location
</div>;
}
こうすることで、URLが変わってもShowLocationButton
は再レンダリングされないため、パフォーマンス的に有利です。さらに、実装としてもこれは問題ありません。ShowLocationButton
のレンダリング後にURLが変化してもShowLocationButton
は再レンダリングされませんが、URLが変わってもhistory
は同じものが使い回されるために、history.location
を参照すれば常に現在のURLとなっています。
結論
現在のURLを得るために使えるuseLocation
とuseHistory
はそれぞれ異なる特徴を持ち、適切に使い分ける必要があります。
useLocation
は「URLが変わると再レンダリングされる」という点が特徴です。URLが変わったらレンダリング結果を変化させなければいけない場合はuseLocation
が適切です。
逆に、useHistory
は「URLが変わっても再レンダリングされない」という特徴を持ちます。現在のURLがレンダリング結果には影響しないが、クリックイベントやuseEffect
といった副作用の中で現在のURLを取得したいという場合はuseHistory
が適切です。
ちなみに、お察しの通り、この記事で扱っていたのはURLが変わっても存在し続けるようなコンポーネントでした。特定のURLでしか表示されないというような場合は正直どちらでも大差ありません。しかし、その場合もこの指針に従うことを強くおすすめします。そうしないと、コードの意図が誤解される恐れがあるからです。
- 一応、
Router
が使うhistory
を動的に変えるようなことをすればhistory
を変えることも可能でしょう。しかし、そのような状況はあまり発生しません。↩