ちゃちゃっとMUIでモーダル作ったときのワーニングを解決した話

## はじめに
MUI(Materiar UI)という、Reactコンポーネントのライブラリがあります。

これを使うことで見た目が整ったコンポーネントを手軽に作成できるようになるので、現場でよく使っています。

その中で、モーダルを使ったときに謎のワーニングがでたので、それを解決した話をまとめます。

## 何が起きたか
### やったこと
以下のように、ちゃちゃっとMUIを使ってモーダルを作成しました。
```react:MyComponent.jsx
import { useState } from "react";
import { Modal, Button } from "@mui/material";
import ChildrenComponent from "./ChildComponent";

const MyComponent = () => {
  const [isOpen, setIsOpen] = useState(false);
  const handleOpenModal = () => setIsOpen(true);
  const handleCloseModal = () => setIsOpen(false);
  return (
    <>
      <Button variant="contained" onClick={handleOpenModal}>
        開く
      </Button>
      <Modal open={isOpen} onClose={handleCloseModal}>
        <ChildrenComponent handleCloseModal={handleCloseModal} />
      </Modal>
    </>
  );
};

export default MyComponent;
```

子コンポーネントは以下のとおりです。
```ChildComponent.jsx
import { Button, Typography, Box } from "@mui/material";

const ChildrenComponent = ({ handleCloseModal }) => {
  return (
    <Box
      sx={{
        padding: "10px",
        margin: "30px auto",
        width: "50%",
        bgcolor: "white",
      }}
    >
      <Typography variant="h6" component="h4">
        テストモーダル
      </Typography>
      <Button variant="contained" onClick={handleCloseModal}>
        閉じる
      </Button>
    </Box>
  );
};

export default ChildrenComponent;
```

画面は以下のようになります。
<img width='70%' src='https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/251137/fbaab07c-542d-cd1b-166e-b09ee90945cd.png'>

モーダルもこの通り。
<img width='70%' src='https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/251137/3709757b-42bb-b695-a628-b401babbd46d.png'>

### 起きたこと
サクサク作れてMUIめっちゃ便利〜と思っていると、コンソールに以下のようなワーニングが出ているではありませんか。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/251137/2515672f-1d7e-b9fa-289b-262c1ea614f9.png)
```
Warning: Failed prop type:
Invalid prop `children` supplied to `ForwardRef(Modal)`. 
Expected an element that can hold a ref.
Did you accidentally use a plain function component for an element instead?
For more information see https://mui.com/r/caveat-with-refs-guide
```

どうやら、`Modal`コンポーネントのchildrenとして設定している`ChildrenComponent`がよくないらしいです。`ref`を受け取れるものでなければならないとのこと。

## 何が原因か
ワーニング内で提示されているURLを確認します。

https://mui.com/r/caveat-with-refs-guide

すべて英語になっていますが、今はChatGPTやClaudeなどのAIがあるので恐れることはないです。

要点をかいつまむと、以下のことが書かれています。
* MUIのコンポーネントの中にはDOMにアクセスするものがいる
* DOMにアクセスするには`ref`が必要
* `ref`を受け取ることができるコンポーネントは限られている

つまり、私が作成した`ChildrenComponent`は`ref`を受け取る事ができないコンポーネントであるために、不適切であるといわれていたわけです。

先程のサイトには`ref`を受け取ることのできるコンポーネントが全部で6種類列挙されています。
現在一般的にコンポーネントを書く際に使われている関数コンポーネントはその中になく、`ref`を受け取ることができません。

※ここでいう`ref`とはReactにおけるDOMを操作するためのものです。
Reactは基本的に仮想DOMとなっていますが、`ref`を使うことで直接DOMを操作することができるようになります。

## どう解決したか
### イケてない解決法
MUIのコンポーネントはすべて`ref`を受け取ることができるため、以下のようにすればサクッと解決できます。

```MyComponent.jsx
import { useState } from "react";
import { Modal, Button, Typography, Box } from "@mui/material";
import ChildrenComponent from "./ChildComponent";

const MyComponent = () => {
  const [isOpen, setIsOpen] = useState(false);
  const handleOpenModal = () => setIsOpen(true);
  const handleCloseModal = () => setIsOpen(false);
  return (
    <>
      <Button variant="contained" onClick={handleOpenModal}>
        開く
      </Button>
      <Modal open={isOpen} onClose={handleCloseModal}>
        <Box
          sx={{
            padding: "10px",
            margin: "30px auto",
            width: "50%",
            bgcolor: "white",
          }}
        >
          <Typography variant="h6" component="h4">
            テストモーダル
          </Typography>
          <Button variant="contained" onClick={handleCloseModal}>
            閉じる
          </Button>
        </Box>
      </Modal>
    </>
  );
};

export default MyComponent;
```

`ChildComponent`内に定義していたMUIのコンポーネントをすべて移植しただけです。
ただ、これだとかなり冗長になってしまうので、できれば別コンポーネントに切り出したいところです。

### いい感じの解決法
一般的によく使われるであろう解決法は、`forwardRef()`でラップすることです。
これは公式サイトでも推奨されています。

どのようにするのか、見てみます。
```react:MyComponent.jsx
import { forwardRef, useState } from "react";
import { Modal, Button } from "@mui/material";
import ChildrenComponent from "./ChildComponent";
// forwardRefを使ってコンポーネントを定義
const ChildrenComponentWithRef = forwardRef((props, ref) => {
  return <ChildrenComponent {...props} ref={ref} />;
});

const MyComponent = () => {
  const [isOpen, setIsOpen] = useState(false);
  const handleOpenModal = () => setIsOpen(true);
  const handleCloseModal = () => setIsOpen(false);
  return (
    <>
      <Button variant="contained" onClick={handleOpenModal}>
        開く
      </Button>
      <Modal open={isOpen} onClose={handleCloseModal}>
        {/* 定義したコンポーネントを設定 */}
        <ChildrenComponentWithRef handleCloseModal={handleCloseModal} />
      </Modal>
    </>
  );
};

export default MyComponent;
```
まず、`forwardRef`を使って`ChildComponent`をラップした、新しいコンポーネントを作製します。
`forwardRef`は`ref`を受け取ることができるので、ワーニングが出なくなります。

そして、ラップした新しいコンポーネントを先程の`ChildComponent`の代わりに指定するだけです。

これだけであっさりワーニングが出なくなりました。

## まとめ
ワーニングはプログラムが動かなくなることはないので、ついつい放置してしまいがちです。
しかし、望ましい状態であることには変わりないので確実に潰しておきたいですね。
（よく配列をmapで回すときにkeyを指定し忘れてワーニングでたりしますよね。）

また、MUIはセンスのない私でもお手軽にきれいなサイトを作ることができるので、使い方を覚えておきたいと思いました。