MUI(Material UI)のLinkを置いただけでは、SPAの画面遷移にはなりません。MUIのLinkが持っているのはタイポグラフィと下線のスタイル、そしてhrefによるネイティブのページ遷移までです。履歴を書き換えて再読み込みなしに画面を切り替える処理はReact Router側が担います。両者をつなぐのがcomponentpropで、最小形は<Link component={RouterLink} to="..." />の一行です。
ここでは最小実装から、テーマに登録してButtonやListItemButtonまで一括で切り替える方法までを扱います。React Router v8でreact-router-domが廃止されたことによるimportの変更と、v8の環境にreact-router-domを入れたときに出るエラーは、@mui/material 9.5.0・react-router 8.5.1で実際に描画して確かめた結果を載せています。
まとめ
- 役割分担:MUIの
Linkは見た目(underlineの既定はalways、colorの既定はprimary)、SPA遷移はReact RouterのLink/NavLink。2つはcomponentpropで1つの要素に合成する。 - 最小実装:
<Link component={RouterLink} to="/concerts">。ButtonやListItemButtonも同じ書き方で、hrefかtoがあればButtonBaseが<a>として描画する。 - import元:React Router v8では
react-routerとreact-router/domから読み込む。v8の環境でnpm i react-router-domを実行すると7系が警告なしに入り、LinkやuseNavigateがRouterを見つけられずに落ちる。 - アプリ全体に効かせる:
LinkBehaviorを作り、createThemeのMuiLinkとMuiButtonBaseに登録する。代わりにTypeScriptではhrefが文字列に限られる。 - 版の現状(2026年11月):MUIはv7の次がv9でv8は欠番、最新は9.5.0。React Routerの最新は8.5.1で、v9は未公開(公式の見積もりは2027年半ば)。
MUIのLinkとReact RouterのLinkの担当範囲
MUIのLinkの既定値とTypographyの継承
MUIのLinkは@mui/material/Linkから読み込み、既定値はunderline="always"(常に下線)、color="primary"、variant="inherit"です。MUI公式のLinksページは、LinkがTypographyの上に作られており、そのpropsをそのまま使えると説明しています。variantの既定がinheritなのは、段落のTypographyの中に置いても文字サイズを崩さないためです。
このため、Linkの内側にさらにTypographyを入れて文字サイズを指定する必要はありません。<Link variant="subtitle1" color="error">のようにLink自身へvariantやcolorを渡せば、Typographyと同じスタイルが当たります。
hrefだけを渡したときのフルリロード
hrefを渡したLinkが描画するのは素の<a>要素です。ブラウザは通常のリンクとして扱うため、別の文書へ通常遷移するとページ全体を読み込み直し、現在のReactの状態は破棄されます。外部サイトへのリンクならこれが正しい挙動ですが、アプリ内の画面切り替えに使うとSPAにした意味がなくなります(同じページ内の#付きリンクへの移動は再読み込みになりません)。
再読み込みなしの遷移は、React RouterのLinkがtopropで担います。現在地を強調表示したいナビゲーションにはNavLinkを使います。アクティブ時に.activeクラスが自動で付き、className/style/childrenには{ isActive }を受け取るコールバックを渡せます。既定では子パスでもアクティブになり、to="/tasks"は/tasks/1を開いているときもアクティブです。完全一致に限りたい項目にはendpropを付けます。ルート(/)は例外で、endがなくても/を開いているときだけアクティブになります(react-router 8.5.1で確認)。ルーティング自体の組み方はReact Router v8の使い方|meta・title設定と基本ルーティング、v7からの変更点で扱っています。
最小実装(component指定とto)
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>
);
}
この例は、アプリがBrowserRouterやRouterProviderの配下にあることを前提とします。名前の衝突を避けるため、React Router側をRouterLinkにリネームしています。MUI側をMuiLinkにする書き方でも動作は同じなので、プロジェクト内でどちらかに揃えておけば足ります。描画されるのは<a href="/concerts">が1つだけで、MUIのクラスとReact Routerのクリック処理が同じ要素に載ります。
React Router v8でのimport変更とreact-router-domの混在
importの書き換え(react-routerとreact-router/dom)
日本語で見つかるMUI連携の記事は、ほぼreact-router-domからimportしています。React Routerのv7からの移行ガイドは「React Router v8 removes the react-router-dom re-export package.」と明記し、npm uninstall react-router-domを指示しています。
// v7まで
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から読み込みます。
v8の環境でreact-router-domを入れると起きるエラー
廃止されたといっても、npmからreact-router-domが消えたわけではありません。7系の更新は続いており、2026年11月3日時点のlatestは7.18.5(10月27日公開)で、非推奨の指定もありません。そのため、古い記事のとおりにnpm i react-router-domを実行すると、v8のプロジェクトでもエラーや警告は出ずにインストールが終わります。
react-router 8.5.1・React 19.3.1の環境で試すと、npm lsの結果は次のとおりでした。react-router-domは自分専用に7系のreact-routerを抱え込みます。
$ npm ls react-router react-router-dom
+-- [email protected]
| `-- [email protected]
`-- [email protected]
この状態で、v8のMemoryRouterの中にreact-router-domのLinkを置き、MUIのLinkのcomponentに渡してrenderToStringで描画すると、次のエラーで止まりました。7系と8系はRouterの情報を渡す仕組み(Context)が別物なので、7系のLinkからは8系のRouterが見えません。
Cannot destructure property 'basename' of 'React10.useContext(...)' as it is null.
react-router-domのuseNavigateを呼んだ場合は「useNavigate() may be used only in the context of a <Router> component.」になります。BrowserRouterで包んでいるのにこのエラーが出るなら、Routerの設定より先に、importがreact-router-domとreact-routerで混ざっていないかと、npm ls react-routerに2つの版が出ていないかを確認してください。直し方はnpm uninstall react-router-domとimportの書き換えです。
動作要件(Node 22.22.0以上/React 19.2.7以上)
react-router 8.5.1のpackage.jsonは、engines.nodeを>=22.22.0、peerDependenciesのreact/react-domを>=19.2.7としています。Viteはピア依存に含まれておらず、Vite 7以上が必要なのはViteプラグインを使うFrameworkモードだけです。BrowserRouterで組んだSPAなら、ビルドツールの版は要件に入りません。
| パッケージ | latest | 公開日(UTC) | 備考 |
|---|---|---|---|
@mui/material |
9.5.0 | 2026-10-09 | v8は欠番 |
@mui/material(next) |
10.0.0-alpha.0 | 2026-10-12 | 試験版 |
react-router |
8.5.1 | 2026-10-27 | v8.0.0は2026-06-17 |
react-router-dom |
7.18.5 | 2026-10-27 | 8系なし |
上げる順番は、React Routerより先にReactです。@mui/material 9.5.0のpeerDependenciesはreact: ^17.0.0 || ^18.0.0 || ^19.0.0で、MUI v9自体はReact 19を求めません。React 19.2.7以上を求めているのはReact Router v8の側です。MUIだけ上げてもReactが18のままならv8には移れません。React Routerのaction・loaderの使い方|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, type LinkProps as RouterLinkProps } from 'react-router';
import type { 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のドキュメントはcomponentに渡すものの条件を「This needs to be able to hold a ref.」としています。公式のコード例もforwardRefのままなので、それに合わせておくのが確実です。
効果範囲(MuiLinkとButtonBase派生コンポーネント)
登録先が2つあるのは、MUIのリンクの入口が2系統あるからです。MuiLinkはテキストリンク、MuiButtonBaseはButton・IconButton・ListItemButton・MenuItem・Tab・CardActionAreaなどクリックできるコンポーネントの土台です。ButtonBaseのLinkComponentは既定が'a'で、説明は「The component used to render a link when the href prop is provided.」です。ここを差し替えると、hrefを渡したButtonBase派生がすべてReact Routerの遷移になります。
作成したthemeを<ThemeProvider theme={theme}>へ渡し、その配下にリンクやボタンを配置します。適用後は、テキストリンクもボタンもhrefを渡すだけで済みます。個別にcomponent={RouterLink}を書いた箇所は従来どおりtoで動くので、移行の途中で書き方が混ざってもすぐには壊れません。UIの部品ごとにリンクの見た目と挙動を揃える設計から相談したい場合は、UI/UXデザインの支援も行っています。
TypeScriptでのhref型制約(文字列限定)
この方式には型の制約があり、ガイドも「This approach has limitations with TypeScript. The href prop only accepts a string.」と警告しています。{ pathname, search, hash }のようなオブジェクト形式の遷移先やstateを渡したい箇所では、テーマに頼らず、その箇所だけcomponent={RouterLink}とtoを書くほうが早いです。テーマ登録は大多数の単純なリンクを楽にする仕組みで、全箇所を1つの書き方に揃える仕組みではありません。
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>
);
}
Buttonでも同じで、<Button component={RouterLink} to="/drafts">を描画すると<a tabindex="0" href="/drafts">が出力され、role="button"は付きません。アンカー要素なので、中クリックや右クリックメニューからの「新しいタブで開く」が機能します。onClickでuseNavigateを呼ぶ実装に置き換えると、EnterキーやSpaceキーでの操作は残りますが、この新しいタブで開く機能は失われます。
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 ?? false;
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>
);
}
公式デモのコメントは、パターン配列を詳細なパスから汎用的なパスの順に並べるよう指定しています。ただし文字列を渡すmatchPathは末尾まで照合するため、matchPath('users', '/users/new')はnullで、usersを先に置いても/users/newを奪いません。順序が結果を変えるのは、複数のパターンが同じURLに一致する場合です。/users/:idは/users/newにもid: "new"として一致するので、['/users/new', '/users/:id']の順に並べます。
ButtonBaseのソースでは、リンク判定はisLink = Boolean(other.href || other.to)です。hrefかtoのどちらかが届けばリンクとして扱われます。届かない場合、ネイティブの<button>ならroleは付きませんが、<span>のような非ネイティブ要素をボタンとして描画するときは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} />
));
BrowserRouterで組むSPA(Declarativeモード)向けのReact Routerのナビゲーションガイドは、「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>
);
}
replace: trueにしているのは、保存後にブラウザの戻るボタンで入力画面へ戻れてしまうのを防ぐためです。第2引数のオプションはモードで使えるものが違い、relative・replace・stateは全モード、flushSync・preventScrollReset・viewTransitionはFrameworkモードとDataモードだけです。このうちviewTransitionは、react-routerのCHANGELOGによると8.5.0で非推奨になりました。React 19.3以降の<ViewTransition>コンポーネントへの移行が推奨されており、NavLinkのisTransitioningとtransitioningクラスも同時に非推奨です。既存の動作は残っているので、新しく遷移アニメーションを組む箇所から切り替えます。
リンクのアクセシビリティ:ボタン型リンク・新しいタブ・aの入れ子
クリックで処理を実行するだけで遷移先を持たない文字列を<Link href="#">で作るのは誤りです。MUI公式のLinksページは、意味のあるhrefがないリンクは<button>要素で描画すべきだとして、<Link component="button" onClick={...}>の例を示しています。描画結果は<button>で、見た目はリンクのまま、キーボードやスクリーンリーダーにはボタンとして伝わります。出力される<button>にはtype属性が付かないので、フォーム内で送信以外の処理に使う場合はtype="button"も指定します。
外部リンクと新しいタブで開く場合
テーマでLinkBehaviorを登録すると、hrefはReact RouterのLinkへ渡ります。React RouterのLinkはhttps://で始まる別オリジンの絶対URLを判別して通常のアンカーとして扱うため、外部URLを渡しても遷移自体は壊れません(8.5.1でto="https://mui.com/x"を描画すると素の<a href="https://mui.com/x">が出力されました)。それでも外部リンクではcomponent="a"を明示しておくと、ルータを通らないリンクであることがコード上で読み取れます。
<Link
component="a"
href="https://mui.com/material-ui/"
target="_blank"
rel="noopener"
aria-label="MUIの公式ドキュメント(新しいタブで開きます)"
>
MUIの公式ドキュメント
</Link>
新しいタブで開くときはtarget="_blank"にrel="noopener"を添えます。公式のLinksページは、新しいウィンドウやタブで開くリンクにはaria-labelでその旨をスクリーンリーダー利用者に伝えるよう求めています。リンクの文言も「こちら」のような汎用語を避け、行き先が分かる語にします。
aタグの入れ子が起きる書き方
MUIのLinkの内側にReact RouterのLinkを直接置くと、出力は<a href="/a"><a href="/b">x</a></a>のようにアンカーの中にアンカーが入ったHTMLになります。HTMLとして不正で、ブラウザが構造を組み替えるためクリック領域が壊れます。Next.js 13以降のnext/linkは自身が<a>を描画するため、<NextLink>で<MuiLink>を包む古い書き方も同じ入れ子になり、ハイドレーションのエラーとして表に出ます。componentで1つの要素として合成するのが原則です。スタイルとロジックを分けるヘッドレスなコンポーネントの考え方は、Base UI(React)とは?Radix UI・MUIとの違いと使い方【v1.9対応】で整理しています。
MUI v8が存在しない理由とv9・次期版の変更点
MUI v8の欠番(v7からv9へ)
「MUI v8」を探しても、Material UIのv8は見つかりません。2026年4月8日のMUIのv9発表ブログは「Material UI moves from v7 straight to v9 (there is no Material UI v8, like there is no v2)」と書いています。Data GridやDate Pickersなどの高機能コンポーネント群であるMUI Xはすでにv8まで進んでいたため、v9でMaterial UIとMUI Xのメジャー番号を揃えた、というのが理由です。npmの@mui/materialにも8系の版はなく、latest-v7(7.3.11)の次は9.0.0です。「MUI X v8」と書かれた資料は、Material UIではなくMUI Xの話です。
nativeButton propの追加とSSR時の属性整合
v9でリンク実装に関わるのが、ButtonBaseとButton・IconButton・ListItemButton・MenuItem・Tab・CardActionAreaなどに追加されたnativeButtonpropです。MUIのv9移行ガイドは目的を「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は残っています。移行対象は余白・配置だけでなく、サイズや文字装飾などのシステムpropにも及びます。MUI v9全体の導入とテーマ設定はMaterial UIとは:v9系の導入からGrid・テーマ設定までコード付きで解説にまとめています。
次のメジャー版(MUI v10・React Router v9)の状況
MUIは2026年10月12日に@mui/material 10.0.0-alpha.0をnpmのnextタグで公開しました。CHANGELOGに載っている破壊的変更はAutocompleteのポップアップの影(elevation)を1から8へ変える1件だけです。9.5.0と10.0.0-alpha.0のLink.jsとButtonBase.jsを取り寄せて比べると、既定値とリンク判定(isLink)の実装は同じでした。この記事のコードは、少なくともalpha.0の時点ではv10でもそのまま動く書き方です。
React Router v9は2026年11月3日時点で公開されておらず、npmのreact-routerのlatestは8.5.1です。React RouterのFuture Changesページは「v9 is currently estimated for mid-2027 when Node 22 reaches EOL.」としており、現時点で示されている最小要件はNode 24以上です。同じページのFuture Flagsは「No future flags yet」、予定された破壊的変更も「No known planned breaking changes yet」です。例外は上で触れたviewTransitionまわりで、8.5.0のリリースノートが「will be removed in v9」と明記しています。この記事のコードは安定版の8.xを基準にしており、v9の移行ガイドが出た時点で見直します。Nodeを22で止めている環境は、v9を待たずに24へ上げておくと移行の手間が1つ減ります。
よくある質問
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ではreact-router-domが廃止され、npmの最新は7系の7.18.5のままです。v8の環境に入れると7系のreact-routerが別に入り、LinkやuseNavigateがRouterを見つけられずにエラーになります。react-routerとreact-router/domへ書き換えてください。
MUIのButtonをリンクにするにはどうしますか?
<Button component={RouterLink} to="/path">と書きます。ButtonBaseはhrefかtoを受け取るとリンクとして扱い、<a>要素で描画します。テーマのMuiButtonBase.defaultProps.LinkComponentにLinkBehaviorを登録していれば、hrefを渡すだけで同じ結果になります。
MUIのLinkを新しいタブで開くにはどうしますか?
target="_blank"とrel="noopener"を付けます。テーマにLinkBehaviorを登録していても外部URLは通常のアンカーとして扱われますが、ルータを通らないリンクだと明示するならcomponent="a"も指定します。新しいタブで開くことはaria-labelなどでスクリーンリーダーにも伝えます。
MUI v8やReact Router v9はありますか?
MUI(Material UI)のv8は存在しません。MUI Xと番号を揃えるため、v7の次はv9になりました。React Router v9は2026年11月3日時点で未公開で、最新は8.5.1です。公式はv9の時期を2027年半ばと見積もっています。MUIは10.0.0-alpha.0が試験版として出ていますが、LinkとButtonBaseの既定値は9.5.0と同じです。
Next.jsやTanStack Routerでも同じ書き方ですか?
考え方は同じで、componentpropに各ルータのリンクを渡します。next/linkは遷移先をhrefで受け取るので、hrefからtoへの読み替えは不要です。TanStack RouterはcreateLinkヘルパを持ち、const CustomLink = createLink(MUILink)のようにMUIのLinkを包んで型付きのリンクを作れます。ルータの比較はTanStack Routerとは?型安全ルーティングの特徴・導入手順とReact Routerとの違いを参照してください。