はじめに
MUI(Materiar UI)という、Reactコンポーネントのライブラリがあります。
これを使うことで見た目が整ったコンポーネントを手軽に作成できるようになるので、現場でよく使っています。
その中で、モーダルを使ったときに謎のワーニングがでたので、それを解決した話をまとめます。
何が起きたか
やったこと
以下のように、ちゃちゃっとMUIを使ってモーダルを作成しました。
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;
子コンポーネントは以下のとおりです。
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;
起きたこと
サクサク作れてMUIめっちゃ便利〜と思っていると、コンソールに以下のようなワーニングが出ているではありませんか。
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を確認します。
すべて英語になっていますが、今はChatGPTやClaudeなどのAIがあるので恐れることはないです。
要点をかいつまむと、以下のことが書かれています。
- MUIのコンポーネントの中にはDOMにアクセスするものがいる
- DOMにアクセスするには
ref
が必要 -
ref
を受け取ることができるコンポーネントは限られている
つまり、私が作成したChildrenComponent
はref
を受け取る事ができないコンポーネントであるために、不適切であるといわれていたわけです。
先程のサイトにはref
を受け取ることのできるコンポーネントが全部で6種類列挙されています。
現在一般的にコンポーネントを書く際に使われている関数コンポーネントはその中になく、ref
を受け取ることができません。
※ここでいうref
とはReactにおけるDOMを操作するためのものです。
Reactは基本的に仮想DOMとなっていますが、ref
を使うことで直接DOMを操作することができるようになります。
どう解決したか
イケてない解決法
MUIのコンポーネントはすべてref
を受け取ることができるため、以下のようにすればサクッと解決できます。
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()
でラップすることです。
これは公式サイトでも推奨されています。
どのようにするのか、見てみます。
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はセンスのない私でもお手軽にきれいなサイトを作ることができるので、使い方を覚えておきたいと思いました。