Material UI Dialog turned to Hook flickering

Viewed 3253

I'm trying to turn Material UI's dialog into a "useDialog" hook so it keeps track of it's own open state.

Unfortunately I've encountered a problem that whenever I update a state further up the hierarchy, the dialog flickers and I'm not exactly sure why and how to circumvent it. I feel like a useRef is needed there somewhere, but I'm not sure. Here's a reproduced minimal example: https://codesandbox.io/s/flickering-dialog-minimal-example-ehruf?file=/src/App.js

And the code in question:

    import React, { useState } from "react";
    import {
      Button,
      Dialog,
      DialogActions,
      DialogContent,
      DialogTitle
    } from "@material-ui/core";

    export default function App() {
      const [openDialog, Dialog] = useDialog();
      const [counter, setCounter] = useState(0);

      return (
        <div>
          <Dialog title="Hello">
            <div>{counter}</div>
            <button onClick={() => setCounter(counter => counter + 1)}>
              Increase
            </button>
          </Dialog>
          <button onClick={openDialog}>Open dialog</button>
        </div>
      );
    }

    const useDialog = () => {
      const [open, setOpen] = useState(false);
      const handleClose = () => {
        setOpen(false);
      };

      const someDialog = ({ title, children }) => {
        return (
          <Dialog open={open} onClose={handleClose}>
            <DialogTitle>{title}</DialogTitle>
            <DialogContent>{children}</DialogContent>
            <DialogActions>
              <Button onClick={handleClose} color="primary">
                Close
              </Button>
            </DialogActions>
          </Dialog>
        );
      };
      return [
        () => {
          setOpen(true);
        },
        someDialog
      ];
    };
2 Answers

The reason the dialog flickers is that a new Dialog component is created on every render(as a result of state change) in App. The old Dialog is unmounted and replaced by the new Dialog.

A rule of thumb is you should never define components while rendering.

That's why I suggest you separate your custom dialog component from useDialog hook:

const MyDialog = ({ open, handleClose, title, children }) => {
  return (
    <Dialog open={open} onClose={handleClose}>
      <DialogTitle>{title}</DialogTitle>
      <DialogContent>{children}</DialogContent>
      <DialogActions>
        <Button onClick={handleClose} color="primary">
          Close
        </Button>
      </DialogActions>
    </Dialog>
  );
};

You can, however, keep some of the logic inside useDialog and reuse them:

const useDialog = () => {
  const [open, setOpen] = useState(false);
  const openDialog = () => {
    setOpen(true);
  };
  const handleClose = () => {
    setOpen(false);
  };
  const props = {
    open,
    handleClose
  };
  return [openDialog, props];
};

Edit material-ui-dialog-turned-to-hook-flickering

More about why returning components from hook can be a bad idea.

Custom hooks are not made for returning a component, instead they are used to create a common logic which will be shared by different components.

In your case I would suggest you to create a common component for your dialog. And use this component wherever you want. Like this:

<CustomDialog open={open}>
// Your jsx here
</CustomDialog>

const CustomDialog = ({children}) => {
return <Dialog open={open} onClose={handleClose}>
            <DialogTitle>{title}</DialogTitle>
            <DialogContent>{children}</DialogContent>
            <DialogActions>
              <Button onClick={handleClose} color="primary">
                Close
              </Button>
            </DialogActions>
          </Dialog>
}

For more information about custom hooks:

https://reactjs.org/docs/hooks-custom.html

Related