49
18

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-09-11

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も良いフレームワークです。ただ、MenuItemTabButtonなどアンカー要素として使う可能性のある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までフォワードしてくるので、この方法は使えません。

【うまくいく】 ButtonLinkで包む

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>
  )
}

【うまくいかない】 MenuItemLinkで包む

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を注入したいみたいです。

【うまくいく】 MenuItemLinkで包んだ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のリンクをうまいこと組み合わせられるようになりました。今回取り扱ったのはButtonMenuItemだけですが同じように包んであげれば他のコンポーネントでも動くと追います(検証はしていない)。本当はLinkのpropsの分離はts-transformer-keysとかで型定義から自動的にやったりできたらいいなと思ったんですが、Next.jsがbabelでTypeScriptのトランスパイルしているので悪あがきできなかった。

49
18
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
49
18

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?