2021/02/15 追記
Material UI v5のリポジトリをあさっていたら、公式でforwardRefするサンプルが追加されていました。このコンポーネントはv4でも使えるはず!
https://github.com/mui-org/material-ui/tree/next/examples/nextjs#the-link-component
はじめに
Material-UIは良いフレームワークです。同じくらいNext.jsも良いフレームワークです。ただ、MenuItemやTab、Buttonなどアンカー要素として使う可能性のあるMaterial-UIのコンポーネントとNext.jsのリンクコンポーネントであるLinkがとても相性が悪いのです。これらをうまく組み合わせて使う方法を考えていきましょう。
【うまくいかない】 Material-UI公式の方法
各コンポーネントのpropsにあるcomponentに対象のリンクコンポーネント入れてねってことらしい。
import { Link } from 'react-router-dom';
import Button from '@material-ui/core/Button';
export default function ButtonRouter() {
return (
<Button color="primary" component={Link} to="/">
With prop forwarding
</Button>
);
}
これをnext/routerでやってみる。
import Link from 'next/link'
import { Button } from '@material-ui/core'
export default function LinkButton() {
return (
<Button component={Link} href="/">
LinkButton
</Button>
)
}
Error: React.Children.only expected to receive a single React element child.と出てエラー。
おそらく、Next.jsのLinkの実装で子要素を1つしか取らない実装でエラーになっているみたい。それに加えてLinkが受け付けないpropsまでフォワードしてくるので、この方法は使えません。
【うまくいく】 ButtonをLinkで包む
Next.jsの公式ドキュメントで示されている通り、passHrefを使って子コンポーネントにhrefを渡してあげる方法でラップするとうまくいきます。
import Link from 'next/link'
import { Button } from '@material-ui/core'
export default function LinkButton() {
return (
<Link href="/" passHref>
<Button>
LinkButton
</Button>
</Link>
)
}
【うまくいかない】 MenuItemをLinkで包む
import Link from 'next/link'
import { Menu, MenuItem } from '@material-ui/core'
export default function Page() {
return <Menu open={true}>
<Link href="/" passHref>
<MenuItem button component="a">Profile</MenuItem>
</Link>
<MenuItem>My account</MenuItem>
<MenuItem>Logout</MenuItem>
</Menu>
}
Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?と怒られます。Menuコンポーネントが子コンポーネントにrefを注入したいみたいです。
【うまくいく】 MenuItemをLinkで包んだforwardRefするコンポーネントを書く
あくまでMaterial-UIやユーザーとしてMenuItemだろうがButtonだろうがrefで触りたいのは中のアンカータグなので、Linkをすっ飛ばしてrefを飛ばせるようにしましょう。Material-UIとNext.jsを併用すると、Linkで困る。がヒントになりますがこれはButtonに対してなのでref問題が表面化していません。これをベースにref問題に対応したコンポーネントを作っていきましょう。
import Link from 'next/link'
import { Menu, MenuItem, MenuItemProps } from '@material-ui/core'
import React from 'react'
type LinkMenuItemProps = Omit<MenuItemProps<'a', { href: string }>, 'component' | 'button'>
const LinkMenuItem = React.forwardRef<HTMLAnchorElement, LinkMenuItemProps>(
function LinkMenuItem(props, forwardedRef) {
const {
href,
...other
} = props
return (
<Link href={href}>
<MenuItem component='a' button ref={forwardedRef} {...other}/>
</Link>
)
})
export default function Page() {
return <Menu open={true}>
<LinkMenuItem href="/">Profile</LinkMenuItem>
<MenuItem>My account</MenuItem>
<MenuItem>Logout</MenuItem>
</Menu>
}
Material-UIとNext.jsを併用すると、Linkで困る。からの変更で一番のミソはforwardRefにジェネリクスでpropsの型とrefの型を渡すためにLinkMenuItemPropsを作成しているところです。Material-UIは各コンポーネントのPropsを拡張可能な形式でエクスポートしています。(Usage of component prop
)
MenuItemProps<'componentで指定するやつ', '追加したい型定義'>で自動的にMenuItemPropsを拡張した型を返してくれます。やりたいことを素直に以下のように書くとcomponentが上書きできなかったり、refの型をanyにしなくてはいけなかったりといろいろと不都合があります。
// refの型を固定できない
type LinkMenuItemProps = Omit<MenuItemProps, 'component' | 'button'> & { href: string }
今回は簡単のためにLinkのpropsのうち、hrefのみ渡して分離していますが、他のやつも必要であれば都度定義して分離してあげてください。
最後に
これでMaterial-UIとNext.jsのリンクをうまいこと組み合わせられるようになりました。今回取り扱ったのはButtonとMenuItemだけですが同じように包んであげれば他のコンポーネントでも動くと追います(検証はしていない)。本当はLinkのpropsの分離はts-transformer-keysとかで型定義から自動的にやったりできたらいいなと思ったんですが、Next.jsがbabelでTypeScriptのトランスパイルしているので悪あがきできなかった。