2220文字
11分
編集

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

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

#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 だけをインストールすれば良くなりました。

diff
+ 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 は利用できません。

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

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

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

#useParams の返り値が optional になった

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

jsx
// 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}`);
  }
}

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

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

#useHistory を useNavigate に置き換える

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

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

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

#Redirect を Navigate に置き換える

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

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

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

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

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

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

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

#Switch を Routes に置き換える

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

diff
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 は削除されました。完全一致でない場合は* を利用します。

diff

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

#相対的なパス

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

diff
- const match = useRouteMatch();

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

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

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

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

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

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

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

jsx
// 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 を意図して付けていなかったパスでは/*を末尾に付ける必要があります。

diff
+ <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 ページが存在しないのであれば、次のように書けます。

diff
  <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 が含まれているかどうかを気にする必要がなくなります。

diff
+ <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 を使えばよりスマートに書けます。

jsx
<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 と置き換わります。パスに応じてヘッダーやフッターを切り替えたいといったケースで便利です。

jsx
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 を参考にして移行してください。

jsx
// 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 を使うべきか

NavLink を使いましょう。

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

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

#パラメーターがほしい

useParams を使いましょう。

#パスの情報がほしい

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

#Data Router を利用している場合

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

#useRoutes を利用している場合

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

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

  return route.path;
};

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

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

jsx
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 のような名前で保存しましょう。

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

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