React Hooks & Material-UI: Unable to Obtain Dialog Ref on First Load in useEffect

Viewed 2224

I'm trying to obtain a reference to the DOM Node of a material-ui Dialog the first time my component loads, but the reference is always undefined.

Here's my approach:

import React, { useEffect, useRef } from "react";
import {
  Dialog,
  DialogTitle,
  DialogContent,
  DialogContentText
} from "@material-ui/core";

export default function App() {
  const dialogRef = useRef();

  useEffect(() => {
    console.log("Dialog Ref");
    console.log(dialogRef.current);
  }, []);

  return (
    <Dialog open ref={dialogRef}>
      <DialogTitle>Ref Test Dialog</DialogTitle>
      <DialogContent>
        <DialogContentText>
          Please check the console to see if a ref to this Dialog was logged.
        </DialogContentText>
      </DialogContent>
    </Dialog>
  );
}

The Material UI Dialog API docs state that "the ref is forwarded to the root element," and my approach is based on the React useRef docs, so it seems like this should be working.

In fact, when I use the same approach on a <div> element, it does work. Am I missing something, or is this a bug in Material UI?

Here's a "working" example of the issue:

https://codesandbox.io/s/hopeful-agnesi-3i0p3?file=/src/App.js

1 Answers

Dialog leverages Modal which in turn leverages Portal. Material-UI's Portal component uses React's createPortal API to render the Dialog outside of the DOM hierarchy of the parent component.

One of the arguments to createPortal is the DOM node to render within. Dialog supports controlling this DOM node (it defaults to document.body) via the container prop. Material-UI gets the DOM node of the container in an effect and after that it renders the Dialog.

So on the initial render of your component, Material-UI's Portal component will identify the container and trigger a subsequent render of the Portal component which will actually render the Dialog.

You can use the disablePortal prop to disable this functionality in which case the Dialog will be rendered immediately and the ref will work as you expect.

If you want to keep the default portal behavior, you can pass a callback function as the ref in order to execute code at the point when the ref is first available. When doing this, it is important to be defensive regarding the argument received. The callback function is called whenever the ref changes which includes it changing back to null during unmount.

There is also an onRendered prop where you could put this functionality; however the docs indicate that it will be deprecated and removed in v5 and to use the ref instead.

Here's a modified version of your sandbox demonstrating using a callback function for the ref:

import React, { useEffect, useRef } from "react";
import {
  Dialog,
  DialogTitle,
  DialogContent,
  DialogContentText
} from "@material-ui/core";

export default function App() {
  const dialogRef = useRef();

  useEffect(() => {
    console.log("Dialog Ref", dialogRef.current);
  }, []);

  return (
    <Dialog
      open
      ref={(node) => {
        dialogRef.current = node;
        // Do your work requiring the node here, but make sure node isn't null.
        console.log("ref function", node);
      }}
      onRendered={() => console.log("onRendered", dialogRef.current)}
      onEntering={() => console.log("onEntering", dialogRef.current)}
      onEntered={() => console.log("onEntered", dialogRef.current)}
    >
      <DialogTitle>Ref Test Dialog</DialogTitle>
      <DialogContent>
        <DialogContentText>
          Please check the console to see if a ref to this Dialog was logged.
        </DialogContentText>
      </DialogContent>
    </Dialog>
  );
}

Edit Dialog ref

Related