O hirunewani blog

Q. Migrate to React Router v6 Q&A まとめ

Created at

React Router v6に移行する際に気を付けるべき点や、移行時に受けた質問をまとめました。

Table of Contents
  1. # Routerの書き方は3種類ある: Data Router, Router, useRoutes
  2. # react-router-dom以外削除する、 @types/react-router-dom v6がない、v6の記述にすると型エラーが出る
  3. # react-routerとhistoryからimportしない
  4. # createBrowserHistoryを使わない
  5. # useParamsの返り値がoptionalになった
  6. # Linkのtoからstateを渡せない、stateを指定するとエラーが出る
  7. # NavLinkの型エラー
  8. # useHistoryをuseNavigateに置き換える
  9. # RedirectをNavigateに置き換える
  10. # SwitchをRoutesに置き換える
  11. # Routeを置き換える
  12. # exactがなくなった
  13. # 相対的なパス
  14. # ページコンポーネントの渡し方、elementを使う
  15. # ネストしたルートでページ遷移できなくなった
  16. # ルート構造を巻き上げる、親ルートでネストルートを宣言する
  17. # Outletを使う、ルート毎に異なるヘッダーやレイアウトを提供したい
  18. # useRouteMatchをuseMatchに置き換える
  19. # useRouteMatchの代わりにuseMatchを使うべきか
  20. # 現在のURLと一致するLinkをハイライトしたい
  21. # ネストしたルートで親のパスが知りたい
  22. # パラメーターがほしい
  23. # パスの情報がほしい
  24. # Data Routerを利用している場合
  25. # useRoutesを利用している場合
  26. # 複数のパターンにマッチしているかを見たい
  27. # useLocationのstateにasを使わずに型を付ける

公式のマイグレーションガイドに記載されている内容については、補完するような内容のみを記載しています。

Routerの書き方は3種類ある: Data Router, Router, useRoutes

大きく分けて以下の3パターンの記述ができます。各々のパターンの情報を見ていると混乱する可能性があるので、それがどのパターンの話なのか認識するようにしてください。

  • Data Router: RouterProvider, createBrowserRouterを使っている場合
  • Router
  • useRoutes

現在、BrowserRouterやRouterを利用しているのであればRouterをそのまま利用した方が、移行が楽だと思います。ただし既にreact-router-dom v6を利用しているリポジトリのほとんどはuseRoutesを利用しています。

react-router-dom以外削除する、 @types/react-router-dom v6がない、v6の記述にすると型エラーが出る

v6からreact-router-domだけをインストールすれば良くなりました。

+ react-router-dom@v6
- react-router-dom
- react-router
- history
- @types/react-router-dom
- @types/react-router

@types/react-routerや@types/react-router-domが入っていると、react-router-domへ同梱されるようになった型と衝突するため、削除する必要があります。

react-routerとhistoryからimportしない

必要なものはreact-router-domからimport出来ます。react-router-domからimportしましょう。

一般に暗黙的にインストールされたパッケージが継続的に利用できるかと、異なるパッケージからexportされたものが同一であることは保証できないため、react-routerとhistoryからのimportは避けた方が良いです。

createBrowserHistoryを使わない

createBrowserHistoryは利用できません。

const history = createBrowserHistory({ basename: process.env.BASENAME });
return (
  <Router history={history}>
    <MyAppRoutes />
  </Router>
);

次のように書き換えてください。

<BrowserRouter basename={process.env.BASENAME}>
  <MyAppRoutes />
</BrowserRouter>

useParamsの返り値がoptionalになった

本当にその値がopitonalになることがないのであれば、assert関数が便利です。

// page component
const { id } = useParams<{id: string}>();
assertIsDefined(id);

// helpers/assertIsDefined.ts
export function assertIsDefined<T>(val: T): asserts val is NonNullable<T> {
  if (val === undefined || val === null) {
    throw new Error(`Expected value to be defined, but received ${val}`);
  }
}

Linkのtoからstateを渡せない、stateを指定するとエラーが出る

state propから渡す形式になりました。

+ <Link to={{ pathname: "/list" }} state={data} >
- <Link to={{ pathname: "/list", state: data }}>
		List
	</Link>
<NavLink
- exact
+ end
- activeStyle={{ ... }}
+ style={({ isActive }) => { ... }}
- activeClassName="actived"
+ className={({ isActive }) => { ... }}
/>

useHistoryをuseNavigateに置き換える

https://reactrouter.com/en/main/upgrading/v5#use-usenavigate-instead-of-usehistory

+ const navigate = useNavigate()
- const history = useHistory()

+ navigate("/home")
- history.push("/home")

RedirectをNavigateに置き換える

RedirectとNavigateはほぼそのまま置き換えられますが、デフォルトの挙動がreplaceからpushに変更されている点には気を付けてください。

+ <Navigate to="about" replace />
- <Redirect to="about" />

+ <Navigate to="home" />
- <Redirect to="home" push />

Redirect元に戻れなくしたい場合は、明示的にreplaceを指定する必要があります。

これはRoutesの都合ですが、NavigateはRedirectと異なりSwitchの代替であるRoutes内に含めることが出来ません。

- <Switch>
-   <Redirect from="about" to="about-us" />
- </Switch>

+ <Routes>
+	  <Route path="about" element={<Navigate to="about-us" replace />} />
+ </Routes>

SwitchをRoutesに置き換える

基本的にそのまま置き換えることが出来ます。ネストされたRoutes内では、相対パスが利用できます。

const App = () => {
	return (
		<BrowserRouter>
+     <Routes>
-     <Switch>
+       <Route path="users/*" element={<Users />} />
-       <Route path="/users"><Users /></Route>
+     </Routes>
-     </Switch>
		</BrowserRouter>
	)
}

const Users = () => {
-	const match = useRouteMatch();
	return (
+   <Routes>
-		<Switch>
+     <Route path=":id" element={<UserProfile />} />
-		  <Route path={`${match.path}/:id`}><UserProfile /></Route>
+   </Routes>
-		</Switch>
	)
}

Routeを置き換える

exactがなくなった

v6からデフォルトが完全一致になったため、exactは削除されました。完全一致でない場合は* を利用します。


+ <Route path="/" element={<Home />} />
- <Route exact path="/"><Home /></Route>
+ <Route path="users/*" element={<Users />} />
- <Route path="/users"><Users /></Route>

相対的なパス

相対的なパスが利用できます。今まで複数のRouteに入る可能性があるコンポーネントや親のパスを伝えたくないようなケースでは、useRouteMatchなどを利用する必要がありましたが、v6では代わりに相対パスが利用できます。

- const match = useRouteMatch();

+ <Route path=":id" element={<UserProfile />} />
- <Route path={`${match.path}/me`}><OwnUserProfile /></Route>

ページコンポーネントの渡し方、elementを使う

v5では、component propやrender prop、childrenなどの渡し方がありました。v6ではelementで渡すことが出来ます。

+ <Route path="/about" element={<About/>} />
- <Route path="/about" component={About} />

型を見るとchildrenでも渡すことが出来るように思えますが、childrenにはRouteを渡すことが想定されるので、elementを利用してください。

ネストしたルートでページ遷移できなくなった

次のような移行をした場合、/users/:id以下のパスへの遷移が出来なくなります。移動しようとすると、親ルートの* にキャッチされ、このケースでは/aboutに遷移します。

// v5
<Route path="/users/:id" component={UsersPageHasNestedRoutes} />
<Route exact path="/about" component={AboutPage} />
<Route path="*" render={() => <Redirect to="about" />} />

// v6
<Route path="/users/:id" element={<UsersPageHasNestedRoutes />} />
<Route path="/about" element={<AboutPage/>} />
<Route path="*" element={<Navigate to="about" replace />} />

完全一致が期待されないv5でexactを意図して付けていなかったパスでは/*を末尾に付ける必要があります。

+ <Route path="/users/:id/*" element={<UsersPageHasNestedRoutes />} />
- <Route path="/users/:id" element={<UsersPageHasNestedRoutes />} />
  <Route path="/about" element={<AboutPage/>} />
  <Route path="*" element={<Navigate to="about" replace />} />

またindexページが存在しないのであれば、次のように書けます。

  <Route path="/users/:id/*" element={<UsersPageHasNestedRoutes />} />
  <Route path="/about" element={<AboutPage/>} />
+ <Route index element={<Navigate to="about" replace />} />
- <Route path="*" element={<Navigate to="about" replace />} />

さらに次のようにルート構造を巻き上げれば、elementにRouteが含まれているかどうかを気にする必要がなくなります。

+ <Route path="/users/:id">
+		<Route index element={<UserPage />} />
+		<Route path="info" element={<UserInfoPage />} />
+		<Route path="code" element={<UserCodePage}/>} />
+	</Route>
- <Route path="/users/:id/*" element={<UsersPageHasNestedRoutes />} />
  <Route path="/about" element={<AboutPage/>} />
  <Route index element={<Navigate to="about" replace />} />

ルート構造を巻き上げる、親ルートでネストルートを宣言する

ルート構造を巻き上げた方が管理しやすくなります。React Route v6ではNested Routesを次のように宣言できます。Data RouterやuseRoutesを使えばよりスマートに書けます。

<Routes>
	<Route index element={<Navigate to="/questions" replace />} />
	<Route path="/questions" elemenet={<Outlet/>}>
		<Route index element={<FormListPage />} />
		<Route path=":questionId" elemenet={<Outlet/>}>
			<Route index element={<EditPage />}>
			<Route path="answers" element={<AnswersPage />}>
			<Route path="debug" element={<DebugPage />}>
		</Route>
	</Route>
</Routes>

Outletを使う、ルート毎に異なるヘッダーやレイアウトを提供したい

https://reactrouter.com/en/main/components/outlet

OutletはRouteで指定されたelementと置き換わります。パスに応じてヘッダーやフッターを切り替えたいといったケースで便利です。

function DefaultLayout() {
  return (
    <div>
      <AppHeader />
      <Outlet />
      <AppFooter />
    </div>
  );
}

function App() {
  return (
    <Routes>
      <Route path="/" element={<DefaultLayout />}>
        <Route path="users" element={<UsersPage />} />
        <Route path="about" element={<AboutPage />} />
      </Route>
    </Routes>
  );
}

useRouteMatchをuseMatchに置き換える

https://reactrouter.com/en/main/upgrading/v5#replace-useroutematch-with-usematch

置き換えろと軽く書かれていますが、パターン引数が必須になり、配列を渡せなくなるなったため、同じような感じでは利用できません。次のQ&Aを参考にして移行してください。

// v5
<Route path="/users">
  <Users />
</Route>;
// users page
const Users = () => {
  const match = useRouteMatch();
  return (
    <Switch>
      <Route path={`${match.path}/:id`}>
        <UserProfile />
      </Route>
    </Switch>
  );
};

// v6
<Route path="/users/*" element={<Users />} />;
// users page
const Users = () => {
  const match = useMatch(/* Error!!! Require path pattern! */);
  //const match = useMatch("/users/:id")
  return (
    <Routes>
      <Route path={`${match.path}/:id`} element={<UserProfile />} />
    </Routes>
  );
};

useRouteMatchの代わりにuseMatchを使うべきか

現在のURLと一致するLinkをハイライトしたい

NavLinkを使いましょう。

ネストしたルートで親のパスが知りたい

相対パスを使いましょう。

パラメーターがほしい

useParamsを使いましょう。

パスの情報がほしい

useLocation()useResolvedPath(””)を検討してください。

Data Routerを利用している場合

useMatchesで代替できます。BrowserRouterなどを利用している場合、使用できません。

useRoutesを利用している場合

BrowserRouterなどを利用している場合とは異なり、親要素のroutesがオブジェクトで定義されていることが期待されるため、次のようなコードを書けばuseRouteMatchの代替できます。

const useMyRouteMatch = (routes: RouteObjectType[]) => {
  const location = useLocation()
  const [{ route }] = matchRoutes(routes, location)

  return route.path
}

複数のパターンにマッチしているかを見たい

patternは自分で指定する必要がありますが、以下のような記述をすればuseRouteMatchに比較的近い使用感で利用できると思います。

const useMyRouteMatch = (patterns: PathPattern<Path> | Path) => {
	const { pathname } = useLocation();
	const match = useMemo(() => {
	  return patterns.find((path) => !!matchPath(path, pathname));
	}, [pathname]);
  return match
}

useLocationのstateにasを使わずに型を付ける

https://github.com/remix-run/react-router/pull/7326#issuecomment-626418225

次のようなファイルをreact-router-dom.d.tstypes/react-router-dom.ts のような名前で保存しましょう。

import { Location } from "react-router-dom";

declare module "react-router-dom" {
  export function useLocation<T = unknown>(): Location & { state: T };
}