🪝

[React] useLocation についての注意点

に公開

概要

React 向けのルーティング用ライブラリ React Router に用意されているフックの一つ、useLocationを使用する上でいくつか躓いた点があったので、諸注意としてまとめた記事になります。

環境

ライブラリ バージョン
create-react-app 5.0.1
react 19.1.0
react-dom 19.1.0
react-router 7.6.2
typescript 4.9.5

なお、以下のコマンドで TypeScript を用いた React アプリを作成した場合について記載します。

npx create-react-app@latest my-app --template typescript

useLocation とは

useLocation の公式 API Reference はこちらです。

Returns the current Location.

useLocation()は現在の Location を返します。

Location の公式 API Reference は以下になります。

An entry in a history stack. A location contains information about the URL path, as well as possibly some arbitrary state and a key.

基本的にはブラウザのページ履歴の一つ一つが Location オブジェクトに当たり、useLocation()はその内最新の(現在の)ものを返します。

こうして返された最新 Location オブジェクトを通じて、現在の URL のパス部分やクエリー文字列、任意の state を取得することができます。

注意点 1. エラー「useLocation() may be used only in the context of a <Router> component.」の対処法

npm install react-router

以上のコマンドで React Router をインストールし、現在の URL などを取得するために useLocation を利用しようとしてみます。

index.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";

const root = ReactDOM.createRoot(
  document.getElementById("root") as HTMLElement
);
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);
App.tsx
import "./App.css";
import { useLocation } from "react-router";

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

export default App;

すると以下のようなエラーが表示されます。

useLocation() may be used only in the context of a <Router> component.

つまり useLocation()<Router>コンポーネントの子孫要素でのみ使用可能であり、React Router によるルーティングが必要となります。

ここで注意すべき点として、エラーメッセージには<Router> component とありますが、<Router>の API Reference には以下のような注意点が記載されています。

Note: You usually won't render a <Router> directly. Instead, you'll render a router that is more specific to your environment such as a <BrowserRouter> in web browsers or a <StaticRouter> for server rendering.

すなわち実際に使用すべきコンポーネントは Web ブラウザでは<BrowserRouter>、サーバーレンダリングでは<StaticRouter>となります。

今回はフロントエンドのみの実装なので<BrowserRouter>を使用し、index.tsxを以下のように書き換えます。

index.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
+ import { BrowserRouter, Routes, Route } from "react-router";

const root = ReactDOM.createRoot(
  document.getElementById("root") as HTMLElement
);
root.render(
  <React.StrictMode>
-     <App />
+     <BrowserRouter>
+       <Routes>
+         <Route path="/app" element={<App />} />
+       </Routes>
+     </BrowserRouter>
  </React.StrictMode>
);

この修正によって<App>内で呼び出している useLocation()<BrowserRouter>の子孫要素となるためエラーが解消されます。

このようにuseLocationを使用するためには React Router によるルーティングが必須となるため、今回の例のように URL のパス部分を取得したいだけの場合はライブラリを使用せずに単にwindows.location.pathnameで取得した方が良い場合もあります。

App.tsx
import "./App.css";
- import { useLocation } from "react-router";

function App() {
-   const location = useLocation();
-   return <div>location.pathname: {location.pathname}</div>;
+   return <div>location.pathname: {window.location.pathname}</div>;
}

export default App;

注意点 2. useLocation()で取得できる state は window.history で取得できる state と異なる

useLocation()の返り値であるLocationオブジェクトには state というプロパティが存在します。

A value of arbitrary data associated with this location.

一方で特別なライブラリを使用せずwindow.historyで呼び出せる History オブジェクトも state というプロパティを持っています。

state は History インターフェイスの読み取り専用プロパティで、履歴スタックの一番上の状態を表す値を返します。

名称は共にstateで履歴スタックの最新の状態を表す値であり任意の key 名を使用できるところも同じですが、この 2 つは違う値を指します。

例として、ボタンによって page1 と page2 を交互に遷移するアプリを実装します。

index.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import { BrowserRouter, Route, Routes } from "react-router";
import Page1 from "./pages/Page1";
import Page2 from "./pages/Page2";

const root = ReactDOM.createRoot(
  document.getElementById("root") as HTMLElement
);
root.render(
  <React.StrictMode>
    <BrowserRouter>
      <Routes>
        <Route path="/page1" element={<Page1 />} />
        <Route path="/page2" element={<Page2 />} />
      </Routes>
    </BrowserRouter>
  </React.StrictMode>
);
Page1.tsx
import { useLocation, useNavigate } from "react-router";

function Page1() {
  const location = useLocation();
  const navigate = useNavigate();
  return (
    <>
      <h1>page1</h1>
      <div>
        <h2>window</h2>
        <div>window.location.pathname:{window.location.pathname}</div>
        <div>window.history.state.stateA: {window.history.state?.stateA}</div>
        <div>
          <button
            onClick={() => {
              window.history.pushState(
                { stateA: "pushedState2" },
                "",
                "/page2"
              );
              window.location.reload();
            }}
          >
            to page2 (window.history.pushState())
          </button>
        </div>
      </div>
      <div>
        <h2>useLocation</h2>

        <div>useLocation pathname: {location.pathname}</div>
        <div>useLocation state.stateA: {location.state?.stateA}</div>
      </div>
      <div>
        <h2>useHistory</h2>
        <div>
          <button
            onClick={() =>
              navigate("/page2", {
                state: { stateA: "navigatedState2" },
              })
            }
          >
            to page2 (useNavigate navigate)
          </button>
        </div>
      </div>
    </>
  );
}

export default Page1;
Page2.tsx
import { useLocation, useNavigate } from "react-router";

function Page2() {
  const location = useLocation();
  const navigate = useNavigate();
  return (
    <>
      <h1>page2</h1>
      <div>
        <h2>window</h2>
        <div>window.location.pathname:{window.location.pathname}</div>
        <div>window.history.state.stateA: {window.history.state?.stateA}</div>
        <div>
          <button
            onClick={() => {
              window.history.pushState(
                { stateA: "pushedState1" },
                "",
                "/page1"
              );
              window.location.reload();
            }}
          >
            to page1 (window.history.pushState())
          </button>
        </div>
      </div>
      <div>
        <h2>useLocation</h2>

        <div>useLocation pathname: {location.pathname}</div>
        <div>useLocation state.stateA: {location.state?.stateA}</div>
      </div>
      <div>
        <h2>useHistory</h2>
        <div>
          <button
            onClick={() =>
              navigate("/page1", {
                state: { stateA: "navigatedState1" },
              })
            }
          >
            to page1 (useNavigate navigate)
          </button>
        </div>
      </div>
    </>
  );
}

export default Page2;

上の動画のようにwindow.history.pushState()window.location.reload()によって最新の履歴スタックに任意の state を設定し画面更新を行った時、設定した state は

  • window.history.stateにより取得することができる。(window.history.state.stateA: の値が更新される)
  • useLocation()の返り値の state で取得することはできない。(useLocation state.stateA: の値は更新されない)

となります。

一方で上の動画のようにuseNavigate()の返す遷移用の関数(例では navigate)を使用して state の設定と画面遷移を行った時、設定した state は

  • window.history.stateにより取得することはできない。(window.history.state.stateA: の値は更新されない)
  • useLocation()の返り値の state で取得することができる。(useLocation state.stateA: の値が更新される)

となります。

つまりuseLocationの返り値の state はあくまでuseNavigateなどと一緒にReact Router の文脈で使用できる stateであり、window.history.pushState()で設定しwindow.history.stateで取得できる state とは別のものということになります。

ここで少し紛らわしい要素として、window.history.pushState()window.location.reload()を使用してページ遷移した際もuseNavigate()の返す遷移用の関数を使用してページ遷移した際も、ともに URL の変更自体は起こるため、

  • window.location.pathnameで取得できる URL のパス部分
  • useLocationの返り値のpathnameで取得できる URL のパス部分

は同じ値を返します。(動画のwindow.location.pathname:useLocation pathname:の値)

あくまでwindowを使用した場合とuseLocationを使用した場合のstate が異なる値であるというのが今回の注意点になります。

株式会社ブレイクエッジ 技術ブログ

Discussion