React Router を v5 から v6 にアップデートしてみました

React Router を v5 から v6 にアップデートしたので、行った作業を振り返ってまとめてみました。レビューがしやすいように、アップデートが最小限のコード修正になるように意識しています。
2021.12.04

MAD 事業部の高橋です。

少し前に React Router を v5 から v6 にアップデートしたので、行った作業を振り返ってまとめてみました。一気に v6 の機能をフルで使うというよりも、最小限の対応で v6 にアップデートするイメージです。

公式サイトのアップデート手順をまずはご参考にしていただき、実際にどんな感じだったのかの雰囲気を感じていただければなと思います。

行った作業は以下となりました。

  • useParams の型の変更に対応
  • <NavLink /> の型の変更に対応
  • <Switch /><Routes /> に変更
  • <Route /> の型の変更に対応
  • <Redirect /><Navigate /> に変更
  • useHistoryuseNavigate に変更
  • useRouteMatchuseMatch に変更

解説していきます。

useParams の型の変更に対応

v5 自体には例えば /user/:userId/content/:contentId のようなパスのパスパラメータを取得する場合には次の用に記述していました。

type ParhParams = {
  userId: string;
  contentId: string;
};
const { userId, contentId } = useParams<Params>();

v6 からの型は次のように定義されるようになりました。

export declare function useParams<Key extends string = string>(): Readonly<
  Params<Key>
>;
export declare type Params<Key extends string = string> = {
  readonly [key in Key]: string | undefined;
};

// つまり以下のように書くと
// userId と contentId の型は `string | undefined` となる
const { userId, contentId } = useParams<"userId" | "contentId">();

そのため、undefined だった場合の対応が必要になりました。userId ?? "" のようにしてしまってもいいのですが、Assertion Functions を使って対応しました。

function assertIsDefined<T>(val: T): asserts val is NonNullable<T> {
  if (val === undefined || val === null) {
    throw new AssertionError(
      `Expected 'val' to be defined, but received ${val}`
    );
  }
}

props の exactend に単純なリネームが必要になります。

場合によっては大きく修正が必要になりそうなものが activeClassNameactiveStyle の廃止です。置き換えの対象プロジェクトが Material UI v5 を使用しており、次のように記述できませんでした。

import { Link } from "@mui/material";
import { NavLink } from "react-router-dom";

<Link
  className={({ isActive }) => (isActive ? "isActive" : "")}
  component={NavLink}
  to="/"
>
  {children}
</Link>;

問題になっていた箇所が数えるほどだったため、次のように対応しました。

<Box sx={{ "> .isActive": { color: "white" } }}>
  <NavLink className={({ isActive }) => (isActive ? "isActive" : "")} {...rest}>
    {children}
  </NavLink>
</Box>

公式で解説されているようにラッパーを用意するのも良さそうです。

<Switch /><Routes /> に変更

アップデートの対象プロジェクトが Switch をネストしていなかったため、単純な置き換えのみでした。

<Route /> の型の変更に対応

exact が不要になったため削除しました。

componentelement に変更するのは単純な作業でしたが、認証に Auth0 を使用していたため、次のようなコンポーネントを作成していました。

const ProtectedRoute = (props: RouteProps) => {
  const { component, ...rest } = props;

  return (
    <Route
      component={withAuthenticationRequired(component as React.ComponentType)}
      {...rest}
    />
  );
};

// <ProtectedRoute exact path="/" component={HomePage} />
// 上記のように使う

element={withAuthenticationRequired(component)} とはできないため、次のように対応しました。

const Protected = withAuthenticationRequired(Outlet);

// <Route element={<Protected />}>
//   <Route path="/" element={<HomePage />} />
// </Route>
// 上記のように使う

<Redirect /><Navigate /> に変更

v5 では push の場合には props で指定が必要ですが、v6 ではデフォルトが push になり、replace の場合に replace の指定が必要となります。

// v5
<Redirect to="/foo" />
<Redirect to="/bar" push />

// v6
<Navigate to="/foo" replace />
<Navigate to="/bar" />

useHistoryuseNavigate に変更

// v5
const history = useHistory();
history.push("/");
history.replace("/");
history.goBack();

// v6
const navigate = useNavigate();
navigate("/");
navigate("/", { replace: true });
navigate(-1);

<Navigate /> と同様にデフォルトが push、replace の場合には指定が必要になります。gogoBackgoForward の場合には引数に number を与えます。

useRouteMatchuseMatch に変更

v5 から v6 になったことで、コンポーネント側から現在の URL の Path Pattern (/user/:userId のようなやつ)を取得する方法が変わりました。

v5 では単純に useRouteMatch を使うだけで良かったのですが、useMatch は Path Pattern を取得するために 確認したい Path Pattern を引数にわたす必要があります。

// v5
const match = useRouteMatch();

// v6
const match = useMatch("/user/:userId");

このため、条件に複数の Path Pattern がある場合には useMatch を使用できません。

例えば、/foo あるいは /bar のどちらかの URL だった場合に何かをしたいような場合、useMatch ではなく matchPath を使う必要があります。

const { pathname } = useLocation();
const match = useMemo(() => {
  return ["/foo", "/bar"].find((path) => !!matchPath(path, pathname));
}, [pathname]);

まとめ

useRoutes<Route /> のネスト、Layout Routes などを使ってより良いコードにすることも、場合によってはできそうです。

ただしチーム開発のパッケージのアップデートの PR には、必要最小限のコードの修正のみに絞っていたほうがレビュワーに優しいのではないかと考えており、この記事もそこに焦点を書いてみました。