uhyo/blog

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を得ることは可能ですが、どちらを使うべきかは場合によって明確に異なります。間違った方を使うと、パフォーマンスが低下したり期待通りに動かなかったりという問題が発生することになります。

useLocationuseHistoryの使い方

さて、useLocationはその名の通り現在のlocationを返すフックです。例えば次のようなコンポーネントがあれば、常に現在のURL(パス)を表示し続けるでしょう。

const ShowLocation = ()=> {
  const location = useLocation();
  return <div>
    {location.pathname}
  </div>;
}

一方、useHistoryhistoryオブジェクトを返します。このオブジェクトはURLを変えたいときに使えるhistory.pushといったメソッドを提供しています。例えば、ボタンがクリックされたときに別のURLに移動したい場合は次のような実装ができます。

const NextPageButton = ()=> {
  const history = useHistory();
  return <button onClick={()=> {
    history.push('/some/url');
  }}>click me!</button>;
}

これらがuseLocationuseHistoryの基本的な使い方です。この記事で特に注目したいのは、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のステート更新関数やuseReducerdispatch関数などは常に同じであることがAPIリファレンスにで明示されています。

従来(5.2.0より前)のreact-routerでは、URLが変わった際に、結果が変わらないのにuseHistoryによる再レンダリングが発生していました。5.2.0ではこれが発生しないように改善されたのです。

useLocationuseHistoryの使い分け方

ここまででuseLocationuseHistoryの違いが分かりましたね。前者は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がアラートで表示されます。こちらの例ではlocationonClick関数の中でのみ使用されています。つまり、実際にレンダリングされる瞬間に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を得るために使えるuseLocationuseHistoryはそれぞれ異なる特徴を持ち、適切に使い分ける必要があります。

useLocationは「URLが変わると再レンダリングされる」という点が特徴です。URLが変わったらレンダリング結果を変化させなければいけない場合はuseLocationが適切です。

逆に、useHistoryは「URLが変わっても再レンダリングされない」という特徴を持ちます。現在のURLがレンダリング結果には影響しないが、クリックイベントやuseEffectといった副作用の中で現在のURLを取得したいという場合はuseHistoryが適切です。

ちなみに、お察しの通り、この記事で扱っていたのはURLが変わっても存在し続けるようなコンポーネントでした。特定のURLでしか表示されないというような場合は正直どちらでも大差ありません。しかし、その場合もこの指針に従うことを強くおすすめします。そうしないと、コードの意図が誤解される恐れがあるからです。


  1. 一応、Routerが使うhistoryを動的に変えるようなことをすればhistoryを変えることも可能でしょう。しかし、そのような状況はあまり発生しません。