React

MUIのLinkとReact Routerで画面遷移を実装する方法|component指定とテーマ一元化(v9/v8対応)

MUIのLinkとReact Routerで画面遷移を実装する方法|component指定とテーマ一元化(v9/v8対応)

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

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

まとめ

  • 役割分担:MUIのLinkは見た目(underline既定はalways、color既定はprimary)、SPA遷移はReact RouterのLink/NavLink。混ぜるのはcomponentpropで行う。
  • 最小実装:<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派生にnativeButtonpropが増え、システム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がtopropで担います。現在地の強調表示が必要なナビゲーションではNavLinkを使い、こちらはアクティブ時に自動で.activeクラスが付き、className/style/childrenに{ isActive }を受け取るコールバックを渡せます。ルート(/)のように他のパスの前方一致になる項目にはendpropを付けて、常にアクティブ判定されるのを防ぎます。ルーティング自体の組み方はReact Routerのmeta・title設定と基本ルーティング|v8対応入門で扱っています。

最小実装(component指定とto)

両者をつなぐのがcomponentpropです。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対応で扱っているデータ読み込み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などボタン系コンポーネントに追加されたnativeButtonpropです。移行ガイドはその目的を「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との違い・使い方をわかりやすく解説で整理しています。

よくある質問

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

どちらか一方ではなく、componentpropで組み合わせます。見た目は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でも同じ書き方ですか?

考え方は同じで、componentpropに各ルータのリンクを渡します。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開発で注目される理由を解説を参照してください。

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

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

関連記事

お気に入りに入れた記事の一覧

この記事は以下の記事からリンクされています

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2026.10.07 テックブログ ファインズの不正アクセスと153万件の予約者情報|予約システムを使う店舗の報告と通知の手順
  2. 2025.08.14 コラム トラクションとは?ビジネスでの意味・指標の目安とgain tractionの使い方
  3. 2026.10.15 テックブログ 銀のさら不正アクセス:最大650万件の対象確認と窓口、パスワード変更・退会の手順
  4. 2026.10.19 テックブログ コミックシーモア不正アクセスで会員情報流出:再開時期・パスワード再設定と運営側のログ点検
  5. 2026.10.16 テックブログ バーガーキング情報漏洩どうしたらいい?約1,118万件の対象確認・補償・退会とメールの真偽

RELATED POSTS 関連記事

目次