---
title: "MUIのLinkとReact Routerで画面遷移を実装する方法｜component指定とテーマ一元化（v9／v8対応）"
url: "https://www.issoh.co.jp/tech/details/4299/"
published: 2024-11-25
updated: 2026-09-10
categories: ["React"]
publisher: "株式会社一創"
---

# MUIのLinkとReact Routerで画面遷移を実装する方法｜component指定とテーマ一元化（v9／v8対応）

MUI（Material UI）の`Link`を置いただけでは、SPAの画面遷移にはなりません。MUIの`Link`が持っているのはタイポグラフィと下線のスタイル、そして`href`によるネイティブのページ遷移までで、履歴を書き換えて再読み込みなしに画面を切り替える処理はReact Router側の担当です。両者をつなぐのが`component`propで、公式が示す最小形は`<Link component={RouterLink} to="..." />`の一行です。

ここでは最小実装から、テーマに登録して`Button`や`ListItemButton`まで一括で切り替える方法までを扱います。あわせて2026年に前提が変わった2点――React Router v8で`react-router-dom`が廃止されたこと、MUI v9で`nativeButton`propが加わったこと――を、公式ドキュメントとソースの記述に沿って整理します。@mui/material 全体の導入手順とv9で変わった点は[Material UIとは：v9系の導入からGrid・テーマ設定までコード付きで解説](https://www.issoh.co.jp/tech/details/17551/)で扱っています。

## まとめ

- **役割分担**：MUIの`Link`は見た目（`underline`既定は`always`、`color`既定は`primary`）、SPA遷移はReact Routerの`Link`／`NavLink`。混ぜるのは`component`propで行う。
- **最小実装**：`<Link component={RouterLink} to="/concerts">`。`Button`や`ListItemButton`も同じ書き方で、`href`または`to`があればButtonBaseがリンクモードに入る。
- **import元が変わった**：React Router v8（2026-06-17）で`react-router-dom`は廃止。`react-router`と`react-router/dom`へ書き換える。npmにv8系の`react-router-dom`は存在しない。
- **アプリ全体に効かせる**：`LinkBehavior`を作って`createTheme`の`MuiLink.defaultProps.component`と`MuiButtonBase.defaultProps.LinkComponent`に登録する。個々の`component`指定が不要になる代わり、TypeScriptでは`href`が文字列限定になる。
- **MUI v9の注意**：ButtonBase派生に`nativeButton`propが増え、システムprop（`mt`等）は削除されて`sx`へ移行した。

## MUIのLinkとReact RouterのLinkの担当範囲

### MUIのLinkの役割とhref指定時のフルリロード

MUIの`Link`は`@mui/material/Link`から読み込むコンポーネントで、既定値は`underline="always"`（常に下線）、`color="primary"`、`variant="inherit"`です。`variant`が`inherit`なのは`Typography`の中に入れても文字サイズが崩れないようにするためで、公式のLink APIにも既定値としてそのまま記載されています。

ここに`href`を渡すと、レンダリングされるのは素の`<a>`要素です。SPAであってもブラウザは通常のリンクとして扱うため、クリックでフルリロードが走り、Reactの状態は破棄されます。外部サイトへのリンクならこれが正しい挙動ですが、アプリ内の画面切り替えに使うと、SPAにした意味が消えます。

### SPA遷移を担うLinkとNavLink

再読み込みなしの遷移は、React Routerの`Link`が`to`propで担います。現在地の強調表示が必要なナビゲーションでは`NavLink`を使い、こちらはアクティブ時に自動で`.active`クラスが付き、`className`／`style`／`children`に`{ isActive }`を受け取るコールバックを渡せます。ルート（`/`）のように他のパスの前方一致になる項目には`end`propを付けて、常にアクティブ判定されるのを防ぎます。ルーティング自体の組み方は[React Routerのmeta・title設定と基本ルーティング｜v8対応入門](/tech/details/7618/)で扱っています。

### 最小実装（component指定とto）

両者をつなぐのが`component`propです。MUIの公式ドキュメント（Routing libraries）は「You can achieve the integration with third-party routing libraries with the `component` prop.」と述べており、デモも次の書き方をそのまま示しています。`to`はMUIが知らないpropですが、`component`に渡した先へそのまま転送されるため機能します。

```
import { Link as RouterLink } from 'react-router';
import Link from '@mui/material/Link';

export function ConcertNav() {
  return (
    <Link component={RouterLink} to="/concerts" underline="hover">
      公演一覧
    </Link>
  );
}
```

名前の衝突を避けるため、React Router側を`RouterLink`にリネームするのが慣例です。逆にMUI側を`MuiLink`にする書き方でも動作は同じで、プロジェクト内でどちらかに統一されていれば問題ありません。

## React Router v8でreact-router-domが廃止された影響

### importの書き換えとパッケージのアンインストール

日本語で見つかるMUI連携の記事はほぼ`react-router-dom`からimportしていますが、この書き方はv8では動きません。React Routerの公式アップグレードガイドは「React Router v8 removes the `react-router-dom` re-export package.」と明記し、`npm uninstall react-router-dom`を指示しています。npm registryの実データもこれと一致し、`react-router`は8.3.0まで進んでいる一方、`react-router-dom`の最新は7.18.1で8系は1件も公開されていません。

```
// v7まで（v8では解決できない）
import { Link, useLocation } from 'react-router-dom';
import { RouterProvider } from 'react-router-dom';

// v8
import { Link, useLocation } from 'react-router';
import { RouterProvider } from 'react-router/dom';
```

振り分けの基準は、DOM固有のAPIかどうかです。`RouterProvider`や`HydratedRouter`のようにブラウザ環境に依存するものは`react-router/dom`、`Link`・`NavLink`・`useNavigate`・`useLocation`など画面遷移で日常的に使うものは`react-router`です。

### 動作要件の確認（Node 22.22.0／React 19.2.7）

v8の最小要件はリリースノートに数値で示されています。Node 22.22.0以上、React 19.2.7以上で、ライブラリはESM専用として公開されるようになりました（tsconfigの`target`／`lib`もES2022）。Viteプラグインを使うFrameworkモードでは、これに加えてVite 7以上が必要です。`react-router` 8.3.0のpackage.jsonを見ると`engines.node`と`react`／`react-dom`のpeerDependenciesだけが指定されており、Viteはピア依存に含まれていません。`BrowserRouter`で組んだSPAなら、ビルドツールの世代は要件から外れます。

| パッケージ              | 最新版    | npm公開日（UTC） | 備考                |
| ------------------ | ------ | ----------- | ----------------- |
| `@mui/material`    | 9.2.0  | 2026-07-03  | v9.0は2026-04-08発表 |
| `react-router`     | 8.3.0  | 2026-07-22  | v8.0.0は2026-06-17 |
| `react-router-dom` | 7.18.1 | 2026-06-29  | v8系なし（廃止）         |

順序として先に上げるべきはReact Routerではなく、Reactです。`@mui/material` 9.2.0のpeerDependenciesは`react: ^17.0.0 || ^18.0.0 || ^19.0.0`で、MUI v9自体はReact 19を要求しません。React 19系を強制しているのはReact Router v8の側で、この非対称を知らずにMUIだけ上げると、遷移まわりが古いままv8の記事どおりに書けない状態が続きます。[React Routerのaction・loaderの使い方｜clientAction/clientLoaderの違いとv8対応](/tech/details/4436/)で扱っているデータ読み込みAPIも同じ要件の上に乗ります。

## テーマへのLinkBehavior登録によるアプリ全体への適用

### LinkBehaviorの実装とcreateThemeへの登録

画面が増えるほど、リンクを書くたびに`component={RouterLink}`を付ける運用は破綻します。MUIのテーマはこの差し替えを一度で済ませる仕組みを持っており、公式ドキュメントもそれを「Global theme Link」として推奨しています。`href`（MUIの語彙）を`to`（React Routerの語彙）へ読み替えるラッパーを1つ作り、テーマに登録します。

```
import * as React from 'react';
import { Link as RouterLink, LinkProps as RouterLinkProps } from 'react-router';
import { LinkProps } from '@mui/material/Link';
import { createTheme } from '@mui/material/styles';

const LinkBehavior = React.forwardRef<
  HTMLAnchorElement,
  Omit<RouterLinkProps, 'to'> & { href: RouterLinkProps['to'] }
>((props, ref) => {
  const { href, ...other } = props;
  // href（MUI）を to（React Router）へ読み替える
  return <RouterLink ref={ref} to={href} {...other} />;
});

const theme = createTheme({
  components: {
    MuiLink: {
      defaultProps: { component: LinkBehavior } as LinkProps,
    },
    MuiButtonBase: {
      defaultProps: { LinkComponent: LinkBehavior },
    },
  },
});
```

ラッパーは`forwardRef`で書きます。React 19では関数コンポーネントが`ref`を通常のpropとして受け取れるため動作自体は成立しますが、MUIのLink APIは`component`に渡すものの条件を「This needs to be able to hold a ref.」と定めており、propTypesのチェックはプレーンな関数コンポーネントを一律に警告します。開発時のコンソールを汚さないためにも`forwardRef`を挟むのが無難です。

### 効果範囲（MuiLinkとButtonBase派生コンポーネント）

登録先を2つ書いているのは、MUIのリンクの入口が2系統あるからです。`MuiLink`はテキストリンク、`MuiButtonBase`は`Button`・`IconButton`・`ListItemButton`・`MenuItem`・`Tab`・`CardActionArea`などクリック可能なコンポーネントの土台で、`LinkComponent`の既定値は`'a'`、説明は「The component used to render a link when the `href` prop is provided.」です。ここを差し替えると、`href`を渡したButtonBase派生がすべてReact Routerの遷移に切り替わります。

登録後は、テキストリンクもボタンも`href`を渡すだけで済みます。ただし個別に`component={RouterLink}`を書いた箇所は従来どおり`to`で動くため、移行途中で書き方が混在してもすぐには壊れません。

### TypeScriptでのhref型制約（文字列限定）

この方式には型の制約があり、公式ドキュメントも警告として「This approach has limitations with TypeScript. The `href` prop only accepts a string.」と書いています。`{ pathname, search, hash }`のようなオブジェクト形式の遷移先や`state`を渡したい箇所では、テーマ経由に頼らず、その箇所だけ`component={RouterLink}`と`to`を明示するほうが早いです。テーマ一元化は「大多数の単純なリンクを楽にする」仕組みで、全箇所を統一する仕組みではないと割り切ったほうが、型エラーとの消耗戦を避けられます。

## Button・メニュー・タブのリンク化

### Drawer／List内メニューのListItemButton実装

サイドメニューの各項目は、`ListItemButton`に`component`と`to`を渡すのが公式デモ（ListRouter）の形です。`ListItem`側に`disablePadding`を付けて、内側の`ListItemButton`が余白ごとクリック領域になるようにします。

```
import { Link } from 'react-router';
import ListItem from '@mui/material/ListItem';
import ListItemButton from '@mui/material/ListItemButton';
import ListItemText from '@mui/material/ListItemText';

export function DraftsMenuItem() {
  return (
    <ListItem disablePadding>
      <ListItemButton component={Link} to="/drafts">
        <ListItemText primary="下書き" />
      </ListItemButton>
    </ListItem>
  );
}
```

アンカー要素になるため、キーボード操作でもEnterで遷移し、中クリックや右クリックの「新しいタブで開く」も機能します。`onClick`で`useNavigate`を呼ぶ実装に置き換えると、この挙動がまとめて失われます。

### Tabsの現在地解決（matchPath）

`Tabs`は選択状態を`value`で管理するため、URLから現在のタブを逆算する必要があります。公式デモ（TabsRouter）は`useLocation`と`matchPath`を組み合わせた`useRouteMatch`を自作し、その結果を`Tabs`の`value`に渡しています。

```
import { Link, matchPath, useLocation } from 'react-router';
import Tabs from '@mui/material/Tabs';
import Tab from '@mui/material/Tab';

function useRouteMatch(patterns) {
  const { pathname } = useLocation();

  for (const pattern of patterns) {
    const possibleMatch = matchPath(pattern, pathname);
    if (possibleMatch !== null) {
      return possibleMatch;
    }
  }
  return null;
}

export function MailTabs() {
  const routeMatch = useRouteMatch(['/inbox/:id', '/drafts', '/trash']);
  const currentTab = routeMatch?.pattern?.path;

  return (
    <Tabs value={currentTab}>
      <Tab label="受信箱" value="/inbox/:id" to="/inbox/1" component={Link} />
      <Tab label="下書き" value="/drafts" to="/drafts" component={Link} />
      <Tab label="ゴミ箱" value="/trash" to="/trash" component={Link} />
    </Tabs>
  );
}
```

公式デモのコメントは、パターン配列を詳細なパスから汎用的なパスへ降順で並べるよう指定しています。`users`・`users/new`・`users/edit`のような入れ子なら`['users/new', 'users/edit', 'users']`の順です。汎用的な`users`を先に置くと、そこで照合が止まって詳細ページのタブが選ばれません。

### role=”button”が付く条件とrole={undefined}

ButtonBaseのソースを見ると、リンク判定は`isLink = Boolean(other.href || other.to)`です。`href`か`to`のどちらかが届いていればリンクとして扱われ、届いていなければボタンとして扱われて`role="button"`が付きます。公式ドキュメントの注記も「If you are not using one of the `href`, `to`, or `component="a"` props, you need to override the `role` attribute.」と述べています。

問題になるのは、遷移先をラッパー内部に固定してMUI側に`to`を渡さない書き方です。この場合はスプレッドの**後ろ**で`role`を打ち消します。順番を逆にすると`{...props}`が`role`を上書きし直すため効きません。

```
const LinkBehavior = React.forwardRef((props, ref) => (
  <RouterLink ref={ref} to="/" {...props} role={undefined} />
));
```

## useNavigateによるプログラム遷移の適用範囲

### 公式が挙げる3つの場面と、モードによる記述の違い

React Routerの`BrowserRouter`で組むSPA（Declarativeモード）のガイドは、`useNavigate`の位置づけをはっきり書いています。「For normal navigation, it’s best to use `Link` or `NavLink`.」であり、`useNavigate`を充てるのはユーザーが操作していないのに遷移が必要な場面――フォーム送信の完了後、無操作によるログアウト、クイズのような時間制のUI――に限る、という整理です。理由は挙動の欠落で、キーボード操作・アクセシビリティのラベル・新しいタブで開く・右クリックメニューといった`<a>`由来の機能はボタン実装では再現されません。

同じ節をFrameworkモードのガイドで読むと記述が変わり、「Usage of this hook should be uncommon.」として例示が2つに減ります。フォーム送信後の遷移は`action`内の`redirect`や`<Form>`に任せるためです。使っているモードによって推奨が違うので、どちらのガイドを読んでいるかを意識してください。

いずれのモードでも、「MUIの`Button`を押したら別ページへ行く」だけなら`useNavigate`は不要で、`<Button component={RouterLink} to="...">`が正解です。`onClick`で遷移させる実装は、保存や認証のように**遷移の前に非同期処理を挟む**ときだけ選びます。

### MUIのButtonのonClickから呼ぶ実装

```
import { useNavigate } from 'react-router';
import Button from '@mui/material/Button';

export function SaveButton({ save }) {
  const navigate = useNavigate();

  return (
    <Button
      variant="contained"
      onClick={async () => {
        await save();
        navigate('/dashboard', { replace: true });
      }}
    >
      保存して戻る
    </Button>
  );
}
```

第2引数のオプションは、モードによって使えるものが違います。公式ドキュメントの分類では`relative`・`replace`・`state`はFramework／Data／Declarativeの全モードで動作し、`flushSync`・`preventScrollReset`・`viewTransition`はFrameworkモードとDataモードのみです。`BrowserRouter`で組んだDeclarativeモードのSPAで`viewTransition`が効かないのは設定ミスではなく仕様なので、遷移アニメーションを前提に設計する場合はモードの選択から考える必要があります。上の例で`replace: true`にしているのは、保存完了後に戻るボタンで入力画面へ戻れてしまう事故を防ぐためです。

## MUI v9の変更点とリンク実装の落とし穴

### nativeButton propの追加とSSR時の属性整合

MUI v9.0は2026年4月8日に発表され、アクセシビリティの既定値と非推奨APIの整理が中心になりました。リンク実装に関わるのが、ButtonBaseと`Button`・`IconButton`・`ListItemButton`・`MenuItem`・`Tab`・`CardActionArea`などボタン系コンポーネントに追加された`nativeButton`propです。移行ガイドはその目的を「to ensure that they are rendered with the correct HTML attributes before hydration, for example during server-side rendering」と説明しています。つまりSSRでハイドレーション前のHTML属性を正しく出すための宣言です。

明示が必要なのは、`component`に渡した自作コンポーネントが既定の要素を置き換える場合――ネイティブの`<button>`から`<div>`のような非インタラクティブ要素へ、あるいはその逆――です。公式例は`<div>`を描画するラッパーに対して`<Button component={CustomButton} nativeButton={false}>`と書いています。省略や実際の要素との不一致があると、開発モードで警告が出ます。

一方、この記事の主役である`<Button component={RouterLink} to="...">`では`nativeButton`を書く必要がありません。`to`が届くと`isLink`が真になり、ButtonBaseは`{...(isLink ? linkProps : buttonProps)}`でリンク側のpropsを適用し、推定した要素との不一致チェックも許容するためです。明示指定を検討すべきなのは、ButtonBaseを直接使っている箇所と、`to`や`href`を渡さずにラッパー内部で遷移先を固定している箇所です。

### システムpropの削除とsxへの移行

v9では`Box`・`Grid`・`Link`・`Stack`・`Typography`などから`mt`・`mr`といったシステムpropが削除され、スタイル指定は`sx`に集約されました。`<Link mr={2}>`のような余白指定は`<Link sx={{ mr: 2 }}>`へ書き換えます。件数が多い場合はcodemodが用意されています。

```
npx @mui/codemod@latest v9.0.0/system-props path/to/folder
```

`underline`や`color`のように`Link`固有のpropは残っているため、影響を受けるのは余白・配置系の記述です。自動変換後は、条件分岐で組み立てているスタイルなど複雑な箇所だけ目視で確認します。

### 外部リンクとaタグ入れ子の切り分け

テーマで`LinkBehavior`を登録すると、`href`もReact Routerの遷移に読み替えられます。ここで外部サイトのURLを`href`に渡すと、React Routerがアプリ内のパスとして解釈しようとして意図しない挙動になります。外部リンクは`component="a"`を明示して素の`<a>`に戻し、別タブで開くなら`rel="noopener"`を添えます。

もう1つ頻発するのが`<a>`の入れ子です。MUIの`Link`の内側にReact Routerの`Link`を直接置くと、アンカーの中にアンカーが入ったHTMLになり、ブラウザが構造を組み替えてクリック領域が壊れます。`component`で**1つの要素として合成する**のが原則で、入れ子で重ねるのは誤りです。ヘッドレスなコンポーネント群でスタイルとロジックを分けたい場合の考え方は[Base UI（React）とは？特徴・Radix UI／MUIとの違い・使い方をわかりやすく解説](/tech/details/10350/)で整理しています。

## よくある質問

### MUIのLinkとReact RouterのLinkはどちらを使えばよいですか？

どちらか一方ではなく、`component`propで組み合わせます。見た目はMUIの`Link`、遷移はReact Routerの`Link`が担当するため、`<Link component={RouterLink} to="/path">`と書くのが公式の推奨です。MUIの`Link`単体に`href`を渡すとフルリロードが発生します。

### react-router-domからimportするコードはもう動きませんか？

React Router v7までは動作しますが、v8では動きません。v8で`react-router-dom`パッケージが削除され、npmにも8系は公開されていない（最新は7.18.1）ためです。`react-router`と`react-router/dom`への書き換えが必要です。

### MUIのButtonをリンクにするにはどうしますか？

`<Button component={RouterLink} to="/path">`と書きます。ButtonBaseは`href`または`to`を受け取るとリンクモードに切り替わり、`<a>`要素として描画されます。テーマに`MuiButtonBase.defaultProps.LinkComponent`を登録済みなら、`href`を渡すだけでも同じ結果になります。

### 現在開いているページのメニュー項目を強調表示したいのですが

テキストリンクなら`NavLink`を使い、`className`や`style`のコールバックが受け取る`isActive`で切り替えます。`Tabs`のように選択状態を`value`で管理するコンポーネントでは、`matchPath`でURLからパターンを解決して`value`に渡します。パターン配列は詳細なパスを先に並べます。

### useNavigateとLinkはどう使い分けますか？

ユーザーがクリックして移動する箇所は`Link`（またはMUIコンポーネント＋`component`指定）、ユーザー操作を伴わない遷移は`useNavigate`です。Declarativeモードのガイドは後者の用途をフォーム送信完了後・無操作によるログアウト・時間制のUIに挙げており、Frameworkモードではさらに絞って`redirect`や`<Form>`を勧めています。`useNavigate`で実装すると新しいタブで開く操作やキーボード操作が失われます。

### Next.jsやTanStack Routerでも同じ書き方ですか？

考え方は同じで、`component`propに各ルータのリンクを渡します。`next/link`は遷移先を`href`で受け取るため、React Routerのような`href`から`to`への読み替えは不要です。オブジェクト形式の`href`を使う場合、MUIのNext.js向け実装例は`href`を`to`に改名したアダプタ（`NextLinkComposed`）を用意しています。TanStack Routerは`createLink`ヘルパを持ち、`const CustomLink = createLink(MUILink)`のようにMUIのLinkを包んで型付きのリンクを作れます。ルータ自体の比較は[TanStack Routerとは何か？概要・特徴・メリットとReact/SPA開発で注目される理由を解説](/tech/details/9913/)を参照してください。

### テーマにLinkBehaviorを登録した後、外部サイトへのリンクはどう書きますか？

`component="a"`を明示して素のアンカーに戻します。テーマ登録後は`href`もReact Routerの遷移として解釈されるため、外部URLをそのまま渡すとアプリ内パスとして扱われます。別タブで開く場合は`target="_blank"`に`rel="noopener"`を添え、スクリーンリーダー向けに挙動が分かるラベルを付けます。

## 関連記事

- [React Routerのmeta・title設定と基本ルーティング｜v8対応入門](/tech/details/7618/)
- [React Routerのaction・loaderの使い方｜clientAction/clientLoaderの違いとv8対応](/tech/details/4436/)
- [Base UI（React）とは？特徴・Radix UI／MUIとの違い・使い方をわかりやすく解説](/tech/details/10350/)
- [TanStack Routerとは何か？概要・特徴・メリットとReact/SPA開発で注目される理由を解説](/tech/details/9913/)
- [MUIとReact Hook Formを用いたインストール手順と設定方法について](/tech/details/4113/)

---

出典: [MUIのLinkとReact Routerで画面遷移を実装する方法｜component指定とテーマ一元化（v9／v8対応）](<https://www.issoh.co.jp/tech/details/4299/>)（株式会社一創）
