前回、MUI の Breadcrumbs のサンプルコードが React Router v6 では動かなかったため、React + React Router v6 + use-react-router-breadcrumbs
を使って、パンくずリストを作ってみました。
React + React Router v6 + use-react-router-breadcrumbs でパンくずリストを作ってみた - メモ的な思考的な
とはいえ、 MUI の Breadcrumbs を使ったパンくずリストも作ってみたくなりました。
Integration with react-router | React Breadcrumbs component - MUI
そこで、React Router v6 でも MUI の Breadcrumbs が動くように実装した時のメモを残します。
目次
環境
前回の記事に比べ、各ライブラリのバージョンは少しずつ上げています。
- React 17.0.2
- React Router 6.0.2
- @mui/material 5.2.2
MUIの例が動作するように実装
ルーティングで Index Routes を実装
前回と異なる点は以下のとおりです。
- 前回とは別のパスを容易
http://localhost:7110/mui-breadcrumbs/1st/2nd/3rd
のようなネストしたルーティングでパンくずリストを表示
- 今回は Index Routes を使用
後者については、例えば、 /mui-breadcrumbs/1st
というパスの場合、前回は
<Route path="1st"> <Route path="" element={<FirstLayer />} /> </Route>
と、ネストした Route で path=""
を設定していました。
一方、今回は公式ドキュメントの Index Routes の形式にのっとり
<Route path="1st"> <Route index element={<MuiFirstLayer />} /> </Route>
と、ネストした Route で path
の代わりに index
を設定します。
全体はこんな感じです。
App.tsx
function App() { return ( <div className="App"> <Routes> <Route path="mui-breadcrumbs" element={<MuiLayout />}> <Route index element={<MuiTop />} /> <Route path="1st"> <Route index element={<MuiFirstLayer />} /> <Route path="2nd"> <Route index element={<MuiSecondLayer />} /> <Route path="3rd"> <Route index element={<MuiThirdLayer />} /> </Route> </Route> </Route> </Route> </Routes> </div> ) }
パンくずリストのコンポーネントを作成
MUI 公式ドキュメントの Breadcrumbs にある Integration with react-router のコード をベースに、React Router v6 で動くように修正します。
大きな違いとしては、 location
は React Router v6 の useLocation
API を使っていることです。
useLocation - React Router | API Reference
あとは
くらいです。
/mui_breadcrumbs/MuiBread.tsx
import useBreadcrumbs from 'use-react-router-breadcrumbs' import {Link as RouterLink, NavLink, useLocation} from 'react-router-dom' import {Breadcrumbs, Link, LinkProps} from '@mui/material' interface LinkRouterProps extends LinkProps { to: string replace?: boolean } // パンくずリストに表示する文言を公式サンプルと変更 const breadcrumbNameMap: {[key: string]: string} = { '/mui-breadcrumbs': 'mui-breadcrumbs ホーム', '/mui-breadcrumbs/1st': '第1階層', '/mui-breadcrumbs/1st/2nd': '第2階層', '/mui-breadcrumbs/1st/2nd/3rd': '第3階層' } const LinkRouter = (props: LinkRouterProps) => <Link {...props} component={RouterLink as any} /> const Component = (): JSX.Element => { // locationは React Router v6 の useLocation API で取得 const location = useLocation() const pathNames = location.pathname.split('/').filter((x) => x) return ( <Breadcrumbs> <LinkRouter underline="hover" color="inherit" to="/"> Home </LinkRouter> {pathNames.map((value, index) => { const last = index === pathNames.length - 1 const to = `/${pathNames.slice(0, index + 1).join('/')}` // リンクの色を分かりやすくするため、サンプルとは color を変更 return last ? ( <div key={to}>{breadcrumbNameMap[to]}</div> ) : ( <LinkRouter underline="hover" color="primary" to={to} key={to}> {breadcrumbNameMap[to]} </LinkRouter> ) })} </Breadcrumbs> ) } export default Component
ページまわりのコンポーネントを作成
レイアウトのコンポーネント
前回の記事と異なり、今回はパスのルートで
<Route path="mui-breadcrumbs" element={<MuiLayout />}>
と、レイアウトコンポーネントである MuiLayout
コンポーネントを設定しています。
この中で、
としています。
/mui_breadcrumbs/MuiLayout.tsx
import {Outlet} from 'react-router-dom' import MuiBread from '@/components/pages/mui_breadcrumbs/MuiBread' const Component = (): JSX.Element => { return ( <> <h1>レイアウト</h1> <MuiBread /> <Outlet /> </> ) } export default Component
各ページのコンポーネント
前回の記事と異なり、パンくずリストのコンポーネントが Layout に移動したため、残ったものを配置します。
また、表示するLinkコンポーネントは MUI のものを使うため、 component
prop に React Router の Link コンポーネントを指定しています。
Third-party routing library - Links - MUI
ただ、 Link
の名前が重複するため、 MUI の公式ドキュメントに従い React Router の Link コンポーネントを RouterLink
として as import します。
/mui_breadcrumbs/MuiTop.tsx
import {Link} from '@mui/material' import {Link as RouterLink} from 'react-router-dom' const Component = (): JSX.Element => { return ( <> <h1>Top Page</h1> <Link to="/mui-breadcrumbs/1st" component={RouterLink as any}> To 1st Layer </Link> </> ) } export default Component
/mui_breadcrumbs/MuiFirstLayer.tsx
import {Link} from '@mui/material' import {Link as RouterLink} from 'react-router-dom' const Component = (): JSX.Element => { return ( <> <h1>First Layer</h1> <Link to="/mui-breadcrumbs/1st/2nd" component={RouterLink as any}> To 2nd Layer </Link> </> ) } export default Component
/mui_breadcrumbs/MuiSecondLayer.tsx
import {Link} from '@mui/material' import {Link as RouterLink} from 'react-router-dom' const Component = (): JSX.Element => { return ( <> <h1>Second Layer</h1> <Link to="/mui-breadcrumbs/1st/2nd/3rd" component={RouterLink as any}> To 3rd Layer </Link> </> ) } export default Component
/mui_breadcrumbs/MuiThirdLayer.tsx
const Component = (): JSX.Element => { return ( <> <h1>Third Layer</h1> </> ) } export default Component
動作確認
以下のようなパンくずリストができました。リンクも MUI のものになっています。
Top
1st Layer
2nd Layer
3rd Layer
ここまでのプルリクはこちらです。
https://github.com/thinkAmi-sandbox/react_mui_with_vite-sample/pull/4
動的ルーティングに対応
MUIの例は、静的ルーティングにのみ対応しています。
例えば、
const breadcrumbNameMap: {[key: string]: string} = { '/mui-breadcrumbs': 'mui-breadcrumbs ホーム', '/mui-breadcrumbs/1st': '第1階層', '/mui-breadcrumbs/1st/:firstId': 'これにはマッチしない', // 追加 }
と、動的ルーティングの設定を追加しても、パンくずリストには表示されません。
そこで、以下のstackoverflowを参考に動的ルーティングの設定も追加してみます。
reactjs - Using React Router DOM Route in Material-UI Breadcrumbs - Stack Overflow
ルーティングに設定を追加
静的ルーティングのみとは別のルートを用意します。
- /parameter-breadcrumbs/root/
:rootId
- /parameter-breadcrumbs/root/
:rootId
/child/:childId
のように、途中や末尾に動的なパスがあるものとします。
<Route path="parameter-breadcrumbs" element={<ParamsLayout />}> <Route index element={<ParamsTop />} /> <Route path="root"> <Route index element={<ParamsRootIndex />} /> <Route path=":rootId"> <Route index element={<ParamsRootDynamic />} /> <Route path="child"> <Route index element={<ParamsChildIndex />} /> <Route path=":childId" element={<ParamsChildDynamic />} /> </Route> </Route> </Route> </Route>
ページまわりのコンポーネントを作成
動的ルーティング時のコンポーネントは、以下のようになります。
なお、React Router の useParams
API を使い、paramsの値を画面に表示しています。
useParams - React Router | API Reference
import {Link} from '@mui/material' import {Link as RouterLink, useParams} from 'react-router-dom' const Component = (): JSX.Element => { const params = useParams() return ( <> <h1>Root Dynamic</h1> <h2>Parameter: {params.rootId}</h2> <Link to="/parameter-breadcrumbs/root/1/child" component={RouterLink as any}> To Child Index </Link> </> ) } export default Component
静的ルーティング時のコンポーネントの作りは、上記で作成した静的ルーティングのコンポーネントと変わらないため、今回は省略します。
詳細は Github に上げたソースコードを参照ください。
https://github.com/thinkAmi-sandbox/react_mui_with_vite-sample/tree/main/src/components/pages/parameter_breadcrumbs
パンくずリストのコンポーネントを作成
MUI のみのバージョン
パンくず名を取得する関数を用意
上記の静的ルーティングの例では、 breadcrumbNameMap
でパンくずに表示するものを指定していました。
const breadcrumbNameMap: {[key: string]: string} = { '/mui-breadcrumbs': 'mui-breadcrumbs ホーム', '/mui-breadcrumbs/1st': '第1階層', '/mui-breadcrumbs/1st/2nd': '第2階層', '/mui-breadcrumbs/1st/2nd/3rd': '第3階層' }
ただ、動的ルーティングの場合は
const breadcrumbNameMap: {[key: string]: string} = { '/parameter-breadcrumbs': 'parameter-breadcrumbs Home', '/parameter-breadcrumbs/root': 'Root Index', '/parameter-breadcrumbs/root/:rootId': 'これにはマッチしない' // 追加 }
としても、マッチしません。
そのため、 breadcrumbNameMap
からパンくずの名前を取得するのではなく、パンくず名を取得する関数 getBreadcrumbsName
を用意します。
今後、ToDoのところを実装していきます。
import {Link as RouterLink, matchPath, useLocation, useMatch} from 'react-router-dom' import {Breadcrumbs, Link, LinkProps} from '@mui/material' interface LinkRouterProps extends LinkProps { to: string replace?: boolean } const staticRouteNameMap: {[key: string]: string} = { '/parameter-breadcrumbs': 'parameter-breadcrumbs Home', '/parameter-breadcrumbs/root': 'Root Index', '/parameter-breadcrumbs/root/:rootId': 'これにはマッチしない' } const getBreadcrumbsName = (to: string) => { const staticRouteName = staticRouteNameMap[to] if (staticRouteName) { return staticRouteName } // TODO: マッチしないものを個別に取得する } const LinkRouter = (props: LinkRouterProps) => <Link {...props} component={RouterLink as any} /> const Component = (): JSX.Element => { // locationは React Router v6 の useLocation API で取得 const location = useLocation() const pathNames = location.pathname.split('/').filter((x) => x) return ( <Breadcrumbs> <LinkRouter underline="hover" color="primary" to="/"> Home </LinkRouter> {pathNames.map((value, index) => { const last = index === pathNames.length - 1 const to = `/${pathNames.slice(0, index + 1).join('/')}` // リンクの色を分かりやすくするため、サンプルとは color を変更 return last ? ( <div key={to}>{getBreadcrumbsName(to)}</div> ) : ( <LinkRouter underline="hover" color="primary" to={to} key={to}> {getBreadcrumbsName(to)} </LinkRouter> ) })} </Breadcrumbs> ) } export default Component
useMatch は使えない
続いて、ToDoの部分を実装していきます。
stackoverflowの例では useRouteMatch
を使っていました。
https://stackoverflow.com/a/59079979
ただ、React Router v6 では useRouteMatch
が無いため、アップグレードガイドにしたがって useMatch
を使ってみます。
Replace useRouteMatch with useMatch - React Router | Upgrading from v5
if (useMatch('/parameter-breadcrumbs/root/:rootId')) { return 'Root Dynamic' } if (useMatch('/parameter-breadcrumbs/root/:rootId/child')) { return 'Child Index' } if (useMatch('/parameter-breadcrumbs/root/:rootId/child/:childId')) { return 'Child Dynamic' }
末尾が動的に変わる場合はうまくいきます。
/parameter-breadcrumbs/root/1
一方、途中が動的に変わる場合はうまくいきません。パンくずの途中の部分まで Child Dynamic
になってしまっています。
/parameter-breadcrumbs/root/1
/child/2
原因は、公式ドキュメントにもある通り、 useMatch
は現在の location
と比較してマッチしているかどうかを見ているためです。
Returns match data about a route at the given path relative to the current location.
matchPath を使う
そこで、別のAPIである matchPath
を使います。こちらはパターンとルートを渡すことで、そのルートがマッチするかどうかを判定してくれます。
matchPath matches a route path pattern against a URL pathname and returns information about the match. This is useful whenever you need to manually run the router's matching algorithm to determine if a route path matches or not. It returns null if the pattern does not match the given pathname.
今回の関数 getBreadcrumbsName
は
/parameter-breadcrumbs /parameter-breadcrumbs/root /parameter-breadcrumbs/root/1 /parameter-breadcrumbs/root/1/child /parameter-breadcrumbs/root/1/child/2
のように順を追ってルートが渡されていく形で呼ばれるため、 matchPath
が使えそうです。
if (matchPath('/parameter-breadcrumbs/root/:rootId', to)) { return 'Root Dynamic' } if (matchPath('/parameter-breadcrumbs/root/:rootId/child', to)) { return 'Child Index' } if (matchPath('/parameter-breadcrumbs/root/:rootId/child/:childId', to)) { return 'Child Dynamic' }
MUI + use-react-router-breadcrumbs のバージョン
MUI のみでは、動的ルーティングが複数ある場合に設定が複雑になりそうでした。
そこで、前々回の記事で使った use-react-router-breadcrumbs
を MUI と組み合わせて使ってみます。
- React + React Router v6 + use-react-router-breadcrumbs でパンくずリストを作ってみた - メモ的な思考的な
- icd2k3/use-react-router-breadcrumbs: tiny, flexible, hook for rendering route breadcrumbs with react-router v6
前々回の記事との違いとしては、
- MUI と組み合わせるので、一番外側に MUI の Breadcrumbs コンポーネントを置く
useBreadcrumbs
する時に、パスとパンくず名を含んだオブジェクト (routes) を渡す- 現在のパスにはリンクを張らない
です。
import useBreadcrumbs from 'use-react-router-breadcrumbs' import {Link as RouterLink} from 'react-router-dom' import {Breadcrumbs, Link, LinkProps} from '@mui/material' interface LinkRouterProps extends LinkProps { to: string replace?: boolean } const routes = [ {path: '/parameter-breadcrumbs', breadcrumb: 'parameter-breadcrumbs Home'}, {path: '/parameter-breadcrumbs/root', breadcrumb: 'Root Index'}, {path: '/parameter-breadcrumbs/root/:rootId', breadcrumb: 'Root Dynamic'}, {path: '/parameter-breadcrumbs/root/:rootId/child', breadcrumb: 'Child Index'}, {path: '/parameter-breadcrumbs/root/:rootId/child/:childId', breadcrumb: 'Child Dynamic'} ] const LinkRouter = (props: LinkRouterProps) => <Link {...props} component={RouterLink as any} /> const Component = (): JSX.Element => { const breadcrumbs = useBreadcrumbs(routes) return ( <> <Breadcrumbs> {breadcrumbs.map(({match, breadcrumb, location}, index) => { // 最後のパスはリンクしないようにする const pathNames = location.pathname.split('/').filter((x) => x) const last = index === pathNames.length return last ? ( <span key={match.pathname}>{breadcrumb}</span> ) : ( <span key={match.pathname}> <LinkRouter underline="hover" color="primary" to={match.pathname}> {breadcrumb} </LinkRouter> </span> ) })} </Breadcrumbs> </> ) } export default Component
以上で実装が終わりました。
ここまでのプルリクはこちらです。
https://github.com/thinkAmi-sandbox/react_mui_with_vite-sample/pull/5
動作確認
いずれも想定通りに動作しています。
/parameter-breadcrumbs/root
/parameter-breadcrumbs/root/1
/parameter-breadcrumbs/root/1/child
/parameter-breadcrumbs/root/1/child/2
ソースコード
Github に上げました。
https://github.com/thinkAmi-sandbox/react_mui_with_vite-sample
また、再掲となりますが、今回の記事で関係するプルリクは以下の2つになります。