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のトランスパイルしているので悪あがきできなかった。