Material-UIとNext.jsのLinkを使うと、refで困る。

# 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公式の方法

https://material-ui.com/guides/composition/#button

各コンポーネントの`props`にある`component`に対象のリンクコンポーネント入れてねってことらしい。

```tsx
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`でやってみる。

```tsx
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の実装で子要素を１つしか取らない実装でエラーになっているみたい。それに加えて`Link`が受け付けないpropsまでフォワードしてくるので、この方法は使えません。

# 【うまくいく】 `Button`を`Link`で包む

[Next.jsの公式ドキュメント](https://nextjs.org/docs/api-reference/next/link#if-the-child-is-a-custom-component-that-wraps-an-a-tag)で示されている通り、passHrefを使って子コンポーネントにhrefを渡してあげる方法でラップするとうまくいきます。

```tsx
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`で包む

```tsx
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で困る。](https://qiita.com/sosukesuzuki/items/62409903a604ef98e7f5)がヒントになりますがこれは`Button`に対してなのでref問題が表面化していません。これをベースにref問題に対応したコンポーネントを作っていきましょう。

```tsx
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で困る。](https://qiita.com/sosukesuzuki/items/62409903a604ef98e7f5)からの変更で一番のミソは`forwardRef`にジェネリクスでpropsの型とrefの型を渡すために`LinkMenuItemProps`を作成しているところです。Material-UIは各コンポーネントのPropsを拡張可能な形式でエクスポートしています。（[Usage of component prop
](https://material-ui.com/guides/typescript/#usage-of-component-prop)）

`MenuItemProps<'componentで指定するやつ', '追加したい型定義'>`で自動的に`MenuItemProps`を拡張した型を返してくれます。やりたいことを素直に以下のように書くとcomponentが上書きできなかったり、refの型をanyにしなくてはいけなかったりといろいろと不都合があります。

```tsx
// refの型を固定できない
type LinkMenuItemProps = Omit<MenuItemProps, 'component' | 'button'> & { href: string }
```

今回は簡単のために`Link`のpropsのうち、hrefのみ渡して分離していますが、他のやつも必要であれば都度定義して分離してあげてください。

# 最後に

これでMaterial-UIとNext.jsのリンクをうまいこと組み合わせられるようになりました。今回取り扱ったのは`Button`と`MenuItem`だけですが同じように包んであげれば他のコンポーネントでも動くと追います（検証はしていない）。本当は`Link`のpropsの分離は[ts-transformer-keys](https://github.com/kimamula/ts-transformer-keys)とかで型定義から自動的にやったりできたらいいなと思ったんですが、Next.jsがbabelでTypeScriptのトランスパイルしているので悪あがきできなかった。