React + React Router v6 + MUI の Breadcrumbs で、動的ルーティングを含むパンくずリストを作ってみた

前回、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 を実装

前回と異なる点は以下のとおりです。

 
後者については、例えば、 /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.

https://reactrouter.com/docs/en/v6/api#usematch

 

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.

https://reactrouter.com/docs/en/v6/api#matchpath

 
今回の関数 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 と組み合わせて使ってみます。

 
前々回の記事との違いとしては、

  • 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つになります。