19
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

React Router v6 にアップグレードしたメモ

Posted at

Router が壊れた

年末に React 案件の package をアップグレードしたら動かなくなった。具体的には、react-router-dom v5.2 -> v6.2 にしたら大量のエラーが発生した。公式ドキュメントを当たると、どうやら現状そのままのコードでは動かないようだ。
React の Router は基本、過去に作ったものを使いまわしていて、その元になったのも、Udemy かどこかで勉強した時のサンプルコードそのまんまで、ふ〜ん程度にしか理解していなかったので、良い機会なので、取り組むことにした。

v6アップグレードガイド

Upgrading from v5 - React Router

ひとまず気まぐれに翻訳した > v6アップグレードガイド

まず、注意点として、

  • 後方互換パッケージ作ってて、そのままでも動くようにするからちょっと待ってて
  • <Prompt> にはまだ非対応

と書いてある。Prompt を使っている、大規模なルートがあるなどの場合は、待つのが良さそう。どちらにも該当しないので、ガイドに沿ってアップグレードすることにした。

手順として、

  1. React v16.8 以上にアップグレードする
  1. React Router を v5.1 にアップグレードする
    1. <Switch>内の<Redirect>を削除する
    2. カスタム<Route>をリファクタリングする
  2. React Router を v6 にアップグレードする

とある。ガイドに従いながらやってみる。

元となるコード

とりあえず、簡単なアプリを作って、それで実行してみる。

$ yarn create react-app sample --template typescript
$ cd sample
$ ncu -u
$ yarn install
$ yarn add react-router-dom@5.1
$ yarn add --dev @types/react-router-dom

ncunpm-check-updatesだ。これで、react-router-dom のバージョン以外はピカピカな typescript の React アプリができた。package.json は以下。

package.json
{
  "name": "sample",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@testing-library/jest-dom": "^5.16.1",
    "@testing-library/react": "^12.1.2",
    "@testing-library/user-event": "^13.5.0",
    "@types/jest": "^27.4.0",
    "@types/node": "^17.0.6",
    "@types/react": "^17.0.38",
    "@types/react-dom": "^17.0.11",
    "react": "^17.0.2",
    "react-dom": "^17.0.2",
    "react-router-dom": "6.2",
    "react-scripts": "5.0.0",
    "typescript": "^4.5.4",
    "web-vitals": "^2.1.2"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": [
      "react-app",
      "react-app/jest"
    ]
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  },
  "devDependencies": {
    "@types/react-router-dom": "^5.3.2"
  }
}

とりあえず、React v16.8 以上にアップグレードする、React Router を v5.1 にアップグレードするは完了していることになる。

ひとまず、App.tsx を書き換える

App.tsx
import { BrowserRouter } from "react-router-dom";
import { Router } from "./router/Router";

export default function App() {
  return (
    <BrowserRouter>
      <Router />
    </BrowserRouter>
  );
}

こいつはリファクタリング対象外だが、一応。

Router は以下の3つを用意した。あるあるな構成かと思う。

Router.tsx
import React, { VFC, memo } from "react";
import { Route, Switch } from "react-router-dom";
import { Home } from "../components/pages/Home";
...
import { MyRouter } from "./MyRouter";
import { PrivateRoute } from "./PrivateRouter";

export const Router: VFC = memo(() => {
  return (
    <Switch>
      {/* 通常の最上位ルート */}
      <Route exact path="/">
        <Home />
      </Route>
      {/* 通常のルート */}
      <Route path="/public">
        <Public />
      </Route>
      {/* パスからパラメータを取るタイプ */}
      <Route
        path="/profile/:id"
        render={({ match }) => <Profile id={match.params.id} />}
      />
      {/* プライベートルート */}
      <PrivateRoute exact={false} path="/secret" loginUser={true}>
        <Secret />
      </PrivateRoute>

      {/* プライベートかつ設定を読み込むルート */}
      <Route
        path="/mypage"
        render={({ match: { url } }) => (
          <Switch>
            {MyRouter.map((route) => (
              <PrivateRoute
                exact={route.exact}
                path={`${url}${route.path}`}
                loginUser={true}
              >
                {route.children}
              </PrivateRoute>
            ))}
          </Switch>
        )}
      />

      {/* 404 */}
      <Route path="*">
        <NotFound />
      </Route>
    </Switch>
  );
});
PrivateRouter.tsx
import { memo, ReactNode, VFC } from "react";
import { Redirect, Route } from "react-router-dom";
//import { useLoginUser } from "../hooks/useLoginUser";

type Props = {
  exact: boolean;
  path: string;
  children: ReactNode;
  loginUser: boolean;
};

export const PrivateRoute: VFC<Props> = memo((props) => {
  const { exact, path, children, loginUser } = props;
  return loginUser ? (
    <Route exact={exact} path={path}>
      {children}
      <div className="notice">
        <i>you are in a private route.</i>
      </div>
    </Route>
  ) : (
    <Redirect to="/" />
  );
});

ログインしてたらページを表示、してなかったらトップページへリダイレクトというヤツ。
今回はただの検証なので、ログインしているかどうかはパラメータ(loginUser)で受け取ることにした。

MyRouter.tsx
import { MyPage } from "../components/pages/my/MyPage";
import { Bookmarks } from "../components/pages/my/Bookmarks";
import { Settings } from "../components/pages/my/Settings";

export const MyRouter = [
  {
    path: "/",
    exact: true,
    children: <MyPage />,
  },
  {
    path: "/bookmarks",
    exact: false,
    children: <Bookmarks />,
  },
  {
    path: "/settings",
    exact: false,
    children: <Settings />,
  },
];

/mypage, /mypage/bookmarks, /mypage/settings のそれぞれの設定。

v5.1 へのアップグレード

path パラメータの取得方法変更

元々 useParams を利用していたが、解説にあったので動作確認のため、付けた。

Router.tsx
-      <Route
-        path="/profile/:id"
-        render={({ match }) => <Profile id={match.params.id} />}
-      />
+      <Route path="/profile/:id" children={<Profile />} />

Profile コンポーネントでは、

Profile.tsx
...
type Props = { id: string };

export const Profile: VFC<Props> = memo((props) => {
  const { id } = props;
  return (
    ...
  );
});

こうだったものを

Profile.tsx
...
import { useParams } from "react-router-dom";

type Props = { id: string };

export const Profile: VFC = memo(() => {
  let { id } = useParams<Props>();
  return (
    ...
  );
});

こうした。

Switch 内の Redirect を削除

PrivateRouter 内に、Redirect があったので、書き換える。

PrivateRouter.tsx
-      <Redirect to="/" />
+      <Route path={path} render={() => <Redirect to="/" />} />

ドキュメントでは、

まとめると、v4/5 から v5.1 へのアップグレードは、以下のようになります。

  • <Route render><Route component> props の代わりに <Route children> を使用する
  • 現在の Location やパラメータなど、ルーターの state にアクセスするには、hooks API を使用する
  • withRouter をすべて hooks に置き換える
  • <Switch> 内に存在しない <Route> は、useRouteMatch で置き換えるか <Switch> で囲む

とあるが、render は例の中にもちょいちょい出てくるので、いったんこれで。

カスタム Route のリファクタリング

<Switch> 内の要素のうち、通常の <Route> 要素でないものを、通常の <Route> に置き換えます。これには、 <PrivateRoute> スタイルのカスタムコンポーネントが含まれます。

ガイドが、さらっと流した!!!
今回の Route 内には、2箇所 PrivateRoute を使っているところがある。1つ目のシンプルな方から直してみる。

Router.tsx
...
      <PrivateRoute exact={false} path="/secret" loginUser={true}>
        <Secret />
      </PrivateRoute>

これを

Router.tsx
...
      <Route
        path="/secret"
        component={() => (
          <PrivateRoute exact={false} path="/secret" loginUser={false}>
            <Secret />
          </PrivateRoute>
        )}
      />
...

こう。結局、<PrivateRoute> 入ってるように見えるけど、<Route> 内の component として指定してある点が違う。これが後々どれほど役立つかと言うと、んー、まぁ?いったん、気にせず、複雑な方の PrivateRoute も同様に書き換える。

Router.tsx
...
      <Route
        path="/mypage"
        render={({ match: { url } }) => (
          <Switch>
            {MyRouter.map((route) => (
              <Route
                path={`${url}${route.path}`}
                component={() => (
                  <PrivateRoute
                    exact={route.exact}
                    path={`${url}${route.path}`}
                    loginUser={true}
                  >
                    {route.children}
                  </PrivateRoute>
                )}
              />
            ))}
          </Switch>
        )}
      />
...

この際、MyRouter.tsx で、この Route の root パスを一番下に持ってこないと、うまく動かない。

MyRouter.tsx
...
export const MyRouter = [
  {
    path: "/bookmarks",
    exact: false,
    children: <Bookmarks />,
  },
  {
    path: "/settings",
    exact: false,
    children: <Settings />,
  },
  {
    path: "/",
    exact: true,
    children: <MyPage />,
  },
];

これでいったん今回の Router の v5.1 へのアップグレードは終わり。

使用したサンプルコードは、こちら(記事そのまんまではない)。

v6 へのアップグレード

まずは、react-router-dom v6 のインストール

$ ncu -u
$ yarn install

これにて、react-router-dom が 5.1 から 6.2 になった(本日現在)。

exact の削除

廃止されたそうなので、削除して回る。ただ削除するだけ。

component を element に変える

順番的には、Switch だが、先に、component を element に変更する。

Router.tsx
...
      {/* 通常の最上位ルート */}
      <Route path="/" element={<Home />} />
      {/* 通常のルート */}
      <Route path="public" element={<Public />} />
      {/* パスからパラメータを取るタイプ */}
      <Route path="profile/:id" element={<Profile />} />
      {/* プライベートルート */}
      <Route
        path="secret"
        element={<PrivateRoute loginUser={false} children={<Secret />} />}
      />
      {/* プライベートかつ設定を読み込むルート */}
      // 後述

      {/* 404 */}
      <Route path="*" element={<NotFound />} />
...

注意点としては、path が絶対パスから相対パスに変更されている点だ。
だいぶスッキリしたが、PrivateRouter が element に対して component を返していると怒られるので、直す。ついでに、 Redirect を Navigate に置き換えた。

PrivateRouter.tsx
import { memo, ReactNode, VFC } from "react";
import { Navigate } from "react-router-dom";
//import { useLoginUser } from "../hooks/useLoginUser";

type Props = {
  children: ReactNode;
  loginUser: boolean;
};

export const PrivateRoute: VFC<Props> = memo((props) => {
  const { children, loginUser } = props;
  return loginUser ? (
    <>
      {children}
      <div className="notice">
        <i>you are in a private route.</i>
      </div>
    </>
  ) : (
    <Navigate to="/" />
  );
});

Switch を Routes に変更

一番外側の Switch は単純に Routes に変更すれば良い。問題は、ここ。

Router.tsx
...
      {/* プライベートかつ設定を読み込むルート */}
      <Route
        path="/mypage"
        render={({ match: { url } }) => (
          <Switch>
            {MyRouter.map((route) => (
              <Route
                path={`${url}${route.path}`}
                component={() => (
                  <PrivateRoute
                    exact={route.exact}
                    path={`${url}${route.path}`}
                    loginUser={true}
                  >
                    {route.children}
                  </PrivateRoute>
                )}
              />
            ))}
          </Switch>
        )}
      />
...

なのだが、element 化したことと、相対パスになったことで?、こんなに複雑に書く必要がなくなり、Switch を利用する必要もなくなった。もっとシンプルに書ける。

Router.tsx
...
      <Route
        path="mypage"
        element={<PrivateRoute loginUser={true} children={<MyPage />} />}
      >
        {MyRouter.map((route) => (
          <Route
            path={route.path}
            element={
              <PrivateRoute loginUser={true} children={route.children} />
            }
          />
        ))}
      </Route>
...

これまで、mypage 以下は全て MyRouter.tsx にまとまっていたが、mypage の root パスが外に出た。
上記コードは、map を使わなければ、以下のような感じになっている。

<Route path="mypage" ...>
  <Route path="bookmarks" ... />
  <Route path="settings" ... />
</Route>

一番外側は、Routes じゃないの?と思うが、Route だ。root パスを外にだしたのと、path に絶対パスはくれるなと怒られるので、MyRouter.tsx を書き換える。

MyRouter.tsx
...
export const MyRouter = [
  {
    path: "bookmarks",
    children: <Bookmarks />,
  },
  {
    path: "settings",
    children: <Settings />,
  },
];

もう分けなくても良いかも。という気になってくる。

その他のリファクタリング

他にも色々あるが、今回は必要なかったので省略。ドキュメントにはコードのサンプルが豊富なので、よく読めば躓くこともないだろう。こういう場合どうなんの?というのがあれば、コメントしていただければ、暇な時に取り組みます。

絶対パスが相対パスになっていることと、正規表現でのマッチが変更になっている点あたりがポイントだろうか。

有効なパス

/groups
/groups/admin
/users/:id
/users/:id/messages
/files/*
/files/:id/*

無効なパス

/users/:id?
/tweets/:id(\d+)
/files/*/cat.jpg
/files-*

最終的なコード

/kurab/UpgradingFromV5ReactRouter

Router.tsx
import React, { VFC, memo } from "react";
import { Route, Routes } from "react-router-dom";
import { Home } from "../components/pages/Home";
import { Public } from "../components/pages/Public";
import { Secret } from "../components/pages/Secret";
import { MyPage } from "../components/pages/my/MyPage";
import { NotFound } from "../components/pages/NotFound";
import { MyRouter } from "./MyRouter";
import { PrivateRoute } from "./PrivateRouter";

export const Router: VFC = memo(() => {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="public" element={<Public />} />

      <Route path="profile/:id" element={<Profile />} />

      <Route
        path="secret"
        element={<PrivateRoute loginUser={false} children={<Secret />} />}
      />
      <Route
        path="mypage"
        element={<PrivateRoute loginUser={true} children={<MyPage />} />}
      >
        {MyRouter.map((route) => (
          <Route
            path={route.path}
            element={
              <PrivateRoute loginUser={true} children={route.children} />
            }
          />
        ))}
      </Route>

      <Route path="*" element={<NotFound />} />
    </Routes>
  );
});
PrivateRouter.tsx
import { memo, ReactNode, VFC } from "react";
import { Navigate } from "react-router-dom";
//import { useLoginUser } from "../hooks/useLoginUser";

type Props = {
  children: ReactNode;
  loginUser: boolean;
};

export const PrivateRoute: VFC<Props> = memo((props) => {
  const { children, loginUser } = props;
  return loginUser ? (
    <>
      {children}
      <div className="notice">
        <i>you are in a private route.</i>
      </div>
    </>
  ) : (
    <Navigate to="/" />
  );
});
MyRouter.tsx
import { Bookmarks } from "../components/pages/my/Bookmarks";
import { Settings } from "../components/pages/my/Settings";

export const MyRouter = [
  {
    path: "bookmarks",
    children: <Bookmarks />,
  },
  {
    path: "settings",
    children: <Settings />,
  },
];

以上。終わり。あけましておめでとうございます。

19
14
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?