Q. Migrate to React Router v6 Q&A まとめ
React Router v6に移行する際に気を付けるべき点や、移行時に受けた質問をまとめました。
- # Routerの書き方は3種類ある: Data Router, Router, useRoutes
- # react-router-dom以外削除する、 @types/react-router-dom v6がない、v6の記述にすると型エラーが出る
- # react-routerとhistoryからimportしない
- # createBrowserHistoryを使わない
- # useParamsの返り値がoptionalになった
- # Linkのtoからstateを渡せない、stateを指定するとエラーが出る
- # NavLinkの型エラー
- # useHistoryをuseNavigateに置き換える
- # RedirectをNavigateに置き換える
- # SwitchをRoutesに置き換える
- # Routeを置き換える
- # exactがなくなった
- # 相対的なパス
- # ページコンポーネントの渡し方、elementを使う
- # ネストしたルートでページ遷移できなくなった
- # ルート構造を巻き上げる、親ルートでネストルートを宣言する
- # Outletを使う、ルート毎に異なるヘッダーやレイアウトを提供したい
- # useRouteMatchをuseMatchに置き換える
- # useRouteMatchの代わりにuseMatchを使うべきか
- # 現在のURLと一致するLinkをハイライトしたい
- # ネストしたルートで親のパスが知りたい
- # パラメーターがほしい
- # パスの情報がほしい
- # Data Routerを利用している場合
- # useRoutesを利用している場合
- # 複数のパターンにマッチしているかを見たい
- # 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の型エラー
<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.ts
やtypes/react-router-dom.ts
のような名前で保存しましょう。
import { Location } from "react-router-dom";
declare module "react-router-dom" {
export function useLocation<T = unknown>(): Location & { state: T };
}