MUI(Material-UI)の Tab コンポーネントと Next.js の next/link を併用する
MUI(Material-UI) は React や Next.js で使えるマテリアルデザインベースのデザインシステムで、これ自体は非常に優秀なのですが、どうも Next.js の next/link との併用にひと手間かける必要があるそうです。
MUI の Link コンポーネントと Next.js の Link コンポーネントを併用する記事はちらほらあり、私もありがたく参考にしています。
ただ今回は MUI の Tab コンポーネントと Next.js の Link コンポーネントを併用 したく、試行錯誤した結果うまくいったので記事にしてみました。
やりたいこと
Next.js でヘッダー部分に MUI の Tabs コンポーネント を使ってタブを表現しています。
useRouter で URL から collection
という変数を取得し、その collection
に対応するタブを選択された状態にします。
各タブには next/link によるリンクが設定されていて、例えば「Apple」がクリックされたら、「/Apple/list」というリンク先に遷移します。
(もちろん遷移先にも同じヘッダーが設定されています)
なお、アプリ全体に ThemeProvider コンポーネント が適応されているものとします。
MUI の Tab コンポーネントと Next.js の next/link を併用したサンプルコード
1. リンク内蔵タブのサンプルコード
import NextLink from "next/link";
import Tab from "@mui/material/Tab";
import React from "react";
import theme from "../lib/theme";
type Props = {
label: string;
href: string;
value?: string;
selected?: boolean;
};
const LinkTab: React.FC<Props> = ({ label, href, value, selected }) => {
return (
<>
<NextLink href={href} legacyBehavior>
{selected ? (
<Tab
label={label}
value={value}
className="Mui-selected"
sx={{ color: theme.palette.primary.main }}
/>
) : (
<Tab label={label} value={value} />
)}
</NextLink>
</>
);
};
export default LinkTab;
基本的には Tab コンポーネントを next/link でラップすれば OK なのですが、どうもそれだけだと装飾が崩れるみたいです。
通常、Tab コンポーネントが選択されている場合は、Mui-selected
という CSS クラスが設定され、テキストカラーは theme.palette.primary.main
で指定された色になります。それらを className
と sx
の属性で手動で設定します。
また、next/link 側は そのまま設定するとリンクの中が <a>
タグになって装飾が適用されない ため、それを打ち消すために legacyBehavior
という属性を設定します。
(これは Next.js の v13 からの仕様です)
2. ヘッダーのサンプルコード
import Paper from "@mui/material/Paper";
import Typography from "@mui/material/Typography";
import Tabs from "@mui/material/Tabs";
import LinkTab from "./LinkTab";
type Props = {
collection?: string;
};
const GlobalHeader: React.FC<Props> = ({ collection }: Props) => {
return (
<>
<Typography component="header">
<Paper square>
<Tabs value={collection || false}>
<LinkTab label="Apple" href="/Apple/list" value="Apple" />
<LinkTab label="Grape" href="/Grape/list" value="Grape" />
<LinkTab label="Banana" href="/Banana/list" value="Banana" />
<LinkTab label="Orange" href="/Orange/list" value="Orange" />
</Tabs>
</Paper>
</Typography>
</>
);
};
export default GlobalHeader;
次にヘッダーのサンプルコードです。
ヘッダーは先述のとおり、useRouter で URL から変数 collection
を取得する想定なので、それをパラメータとして受け入れられるようにしています。
Tabs コンポーネントの仕様として、value
属性に入っている値を持っているタブが選択扱いになり、value
が false
の場合は未選択の扱いになります。
なので、パラメータに値が入っていない場合は false
を設定します。
(なお、LinkTab
側で設定していた selected
パラメータは自動で渡されるので、特に設定しなくても大丈夫です)
3. ページのサンプルコード
import type { NextPage } from "next";
import GlobalHeader from "../../components/GlobalHeader";
import { useRouter } from "next/router";
const ListView: NextPage = () => {
const router = useRouter();
const { collection } = router.query;
return (
<>
<GlobalHeader
collection={
collection
? Array.isArray(collection)
? collection.join("")
: collection
: undefined
}
/>
</>
);
};
export default ListView;
最後にページのサンプルコードです。
先述のとおり useRouter を使いますが、これで取得できる値は string|string[]|undefined
型なので、三項演算子を使って条件分岐させてヘッダーにパラメータを渡します。
これで Tab と next/link の併用ができました!
おまけ①:外部リンクにも対応する
念のため、外部リンクにも対応しておきます。
外部リンクの場合はそもそも next/link を使う必要がないので、MUI 公式サイトのサンプルの書き方が使えます。
外部リンクの場合は、URL の先頭が「http」なので、これで条件分岐してあげれば OK です。
また、外部リンクでよくやる対応として、target と rel を設定しておきます。
import NextLink from "next/link";
import Tab from "@mui/material/Tab";
import React from "react";
import theme from "../lib/theme";
type Props = {
label: string;
href: string;
value?: string;
selected?: boolean;
};
const LinkTab: React.FC<Props> = ({ label, href, value, selected }) => {
return (
<>
+ {href.startsWith("http") ? (
+ <Tab
+ label={label}
+ href={href}
+ component="a"
+ target="_blank"
+ rel="noopener noreferrer"
+ />
+ ) : (
<NextLink href={href} legacyBehavior>
{selected ? (
<Tab
label={label}
value={value}
className="Mui-selected"
sx={{ color: theme.palette.primary.main }}
/>
) : (
<Tab label={label} value={value} />
)}
</NextLink>
+ )}
</>
);
};
export default LinkTab;
・・・
<LinkTab label="Google" href="https://www.google.co.jp" />
・・・
これで外部リンクにも対応できました!
おまけ②:App Router に対応する
Next.js の新機能で App Router というものがあり、これを使うとルーティングの方式が変わります。上記のコードは App Router 非対応のものなので、App Router 対応のコードも書いてみます。
LinkTab.tsx
変更なし。
GlobalHeader.tsx
先頭に use client
を付与します。
+"use client";
import Paper from "@mui/material/Paper";
import Typography from "@mui/material/Typography";
import Tabs from "@mui/material/Tabs";
import LinkTab from "./LinkTab";
type Props = {
collection?: string;
};
const GlobalHeader: React.FC<Props> = ({ collection }: Props) => {
return (
<>
<Typography component="header">
<Paper square>
<Tabs value={collection || false}>
<LinkTab label="Apple" href="/Apple/list" value="Apple" />
<LinkTab label="Grape" href="/Grape/list" value="Grape" />
<LinkTab label="Banana" href="/Banana/list" value="Banana" />
<LinkTab label="Orange" href="/Orange/list" value="Orange" />
</Tabs>
</Paper>
</Typography>
</>
);
};
export default GlobalHeader;
list.tsx
まずファイルパスとファイル名ですが、/pages/[collection]/list.tsx
から /app/[collection]/list/page.tsx
に変更します。
その上で以下のように変更します。
+"use client";
import type { NextPage } from "next";
-import GlobalHeader from "../../components/GlobalHeader";
-import { useRouter } from "next/router";
+import GlobalHeader from "@/components/GlobalHeader";
+import { useParams } from "next/navigation";
const ListView: NextPage = () => {
- const router = useRouter();
- const { collection } = router.query;
+ const params = useParams().collection;
+ const collection = params
+ ? Array.isArray(params)
+ ? params.join("")
+ : params
+ : undefined;
return (
<>
<GlobalHeader
collection={
collection
- ? Array.isArray(collection)
- ? collection.join("")
- : collection
- : undefined
}
/>
</>
);
};
export default ListView;
App Router ではページを app フォルダ配下の page.tsx
というファイル名で表現するため、そのように変更しています。
また、App Router では useRouter
が新しい関数 useParams
に置き換わったので、それを使っています。
以上です。
Discussion