💨

【React】ダイアログ開くのにuseRef使うやつ…いねえよなぁ!!

2022/12/18に公開2

ダイアログを表示するときに良くないやり方(useRef使って)やってたので自戒を込めて記事に
します。

以下抜粋

宣言的に行えるものには ref を使用しないでください。
例えば、Dialog コンポーネントに open() と close() メソッドを実装するかわりに、isOpen プロパティを渡してください。
Ref を使いすぎない
最初はアプリ内で「何かを起こす」ために ref を使いがちかもしれません。そんなときは、少し時間をかけて、コンポーネントの階層のどこで状態を保持すべきかについて、よりしっかりと考えてみてください。多くの場合、その状態を「保持する」ための適切な場所は階層のより上位にあることが明らかになるでしょう。具体例については state のリフトアップガイドを参照してください。

https://ja.reactjs.org/docs/refs-and-the-dom.html

公式が言うには基本的にだめらしいです。

今回のソース↓全体
https://codesandbox.io/s/dialog-un63go?file=/src/App.js

ダイアログをrefを使ってオープンクローズすると↓のようになります。(mamterial ui使ってますが本題ではないので気にしなくて大丈夫です)

親コンポーネント

parent.js
import React, { useRef } from "react";
import { Button } from "@mui/material";
import { DialogRef } from "./DialogRef";

export const ParentDialogRef = () => {
  const dialogRef = useRef();

  const handleClickOpen = () => {
    dialogRef.current.openDialog();
  };

  return (
    <>
      <Button variant="outlined" onClick={handleClickOpen}>
        Open dialog ref
      </Button>
      <DialogRef ref={dialogRef} />
    </>
  );
};

子コンポーネント

DialogRef.js
import React, { useState, forwardRef, useImperativeHandle } from "react";
import { Dialog, DialogTitle } from "@mui/material";

export const DialogRef = forwardRef((props, ref) => {
  const [open, setOpen] = useState(false);

  useImperativeHandle(ref, () => ({
    openDialog() {
      setOpen(true);
    }
  }));

  const handleClose = () => {
    setOpen(false);
  };

  return (
    <>
      <Dialog onClose={handleClose} open={open}>
        <DialogTitle>dailog ref open</DialogTitle>
        AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
      </Dialog>
    </>
  );
});

これをプロパティを渡すように変更すると↓のようになります。プロパティで実現出来るときはなるべくrefは使わないようにしましょう。

親コンポーネント

import React, { useState } from "react";
import { Button } from "@mui/material";
import { SimpleDialog } from "./SimpleDialog";

export const ParentDialogSimple = () => {
  const [open, setOpen] = useState(false);

  const handleClickOpenSimple = () => {
    setOpen(true);
  };

  const handleClose = () => {
    setOpen(false);
  };

  return (
    <>
      <Button variant="outlined" onClick={handleClickOpenSimple}>
        Open simple dialog
      </Button>
      <SimpleDialog isOpen={open} onClose={handleClose} />
    </>
  );
};

子コンポーネント

import React from "react";
import { Dialog, DialogTitle } from "@mui/material";

export const SimpleDialog = ({ isOpen, onClose }) => {
  const handleClose = () => {
    onClose();
  };

  return (
    <>
      <Dialog onClose={handleClose} open={isOpen}>
        <DialogTitle>simple dailog open</DialogTitle>
        BBBBBBBBBBBBBBBBBBBBBBBBB
      </Dialog>
    </>
  );
};

見比べると分かりますがrefを使う方法だと子コンポーネントにuseStateがあって、refを使わないと親コンポーネントにuseStateがありますね。最初の抜粋↓にもあったように親がstateを持つのが適切な場合がほとんどらしいのでrefを使うときは一度考え直すと良いと思います。

多くの場合、その状態を「保持する」ための適切な場所は階層のより上位にあることが明らかになるでしょう

以上。

Discussion