React Material UI - Snackbar closes when Dialog closes but its not supposed to

Viewed 3368

Material ui dialog closes the snackbar along with it.

This is a weird problem so I created a demo to demonstrate the issue:

https://codesandbox.io/s/react-hooks-counter-demo-v20w3

I am passing states from parent to child so the parent state can update based on the child

 <Child openDialog={openDialog} setOpenDialog={setOpenDialog} />

In the child, I am using these as below

export default function Child({openDialog, setOpenDialog}) {

The button in the child is supposed to close only the dialog but it closes snackbar too.

  <button 
  onClick={() => dialogClick()}
  >Click this dialog button
  </button>

Line 12 in the child has setOpenSnack("true"); This only works when I comment out line 13 setOpenDialog(false);

  const dialogClick = (event) => {
    setMsg("This should close dialog only and open the snackbar");
    setOpenSnack(true);
    setOpenDialog(false);
  };

This behavior is only when I split it into child and parent.

In short, why does setOpenSnack(true); in the child not work?

3 Answers

Move your Snackbar to your parent component so that it's not dependent on Dialog's lifecycle.

App.js

import DialogBody from "./child";

function App() {
  const [openDialog, setOpenDialog] = useState(false);
  const [openSnack, setOpenSnack] = useState(false);
  const [msg, setMsg] = useState("nothing");

  function doApiCall(formData) {
    // do your API calls here
    console.log(formData);

    // when done hide dialog, show snackbar
    setOpenDialog(false);
    setOpenSnack(true);
  }

  const fireOnClick = (event) => {
    setMsg("you clicked a button");
    setOpenDialog(true);
  };

  const handleCloseSnack = (event, reason) => {
    if (reason === "clickaway") {
      return;
    }

    setOpenSnack(false);
  };

  function Alert(props) {
    return <MuiAlert elevation={6} variant="filled" {...props} />;
  }

  return (
    <div className="App">
      <h3>click button below to open dialog</h3>

      <button onClick={() => fireOnClick()}>Click me</button>

      <Dialog open={openDialog}>
        <DialogTitle>This is a dialog</DialogTitle>
        <DialogBody doApiCall={doApiCall} />
      </Dialog>

      <Snackbar
        open={openSnack}
        autoHideDuration={6000}
        onClose={handleCloseSnack}
      >
        <Alert onClose={handleCloseSnack} severity="error">
          {msg}
        </Alert>
      </Snackbar>
    </div>
  );
}

Child.js

export default function DialogBody({ doApiCall }) {
  const [name, setName] = React.useState("");

  function onChange(event) {
    setName(event.target.value);
  }

  return (
    <div>
      <input type="text" value={name} onChange={onChange} />
      <button onClick={() => doApiCall({ name })}>
        Click this dialog button
      </button>
    </div>
  );
}

Our DialogBody component only accepts a prop called doApiCall from our parent, which is invoked by clicking the Click this dialog button.

doApiCall might be an asynchronous call to your backends API which when resolves will hide the dialog and sets the snackbar to open.

Edit React Hooks Counter Demo

When the dialog is closed, the Child component is dismantled. This justifies the disappearance of the Snack. Try this to see:

index.js

import React, { useState } from "react";
import ReactDOM from "react-dom";
import DialogTitle from "@material-ui/core/DialogTitle";
import Dialog from "@material-ui/core/Dialog";
import Child from "./child";
import Snackbar from "@material-ui/core/Snackbar";
import MuiAlert from "@material-ui/lab/Alert";


import "./styles.css";

function App() {
  const [msg, setMsg] = useState("nothing");
  const [openSnack, setOpenSnack] = useState(false);
  const [openDialog, setOpenDialog] = useState(false);
  //Close snackbar except when clickaway

  const fireOnClick = (event) => {
    setMsg("you clicked a button");
    setOpenDialog(true);
    //setOpenSnack(true);
  };

  const snackState = (value) => {
          setOpenSnack(value)
  }

  const snackMessage = (value) => {
    setMsg(value)
}

  const handleCloseSnack = (event, reason) => {
    if (reason === "clickaway") {
      return;
    }
    setOpenSnack(false);
  };

  function Alert(props) {
    return <MuiAlert elevation={6} variant="filled" {...props} />;
  }

  const closeDialogOnly = (event) => {
    setMsg("you closed dialog only");
    setOpenDialog(false);
  };

  return (
    <div className="App">
      <h3>click button below to open dialog</h3>

      <button onClick={() => fireOnClick()}>Click me</button>

      <Dialog onClose={closeDialogOnly} open={openDialog}>
        <DialogTitle>This is a dialog</DialogTitle>
        <Child snackbarStat={snackState} snackMessage={snackMessage} setOpenDialog={setOpenDialog} />
      </Dialog>

      <Snackbar
        open={openSnack}
        autoHideDuration={6000}
        onClose={handleCloseSnack}
      >
        <Alert onClose={handleCloseSnack} severity="error">
          {msg}
        </Alert>
      </Snackbar>
    </div>
  );
}


const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

child.js

    import React from "react";


export default function Child({ snackbarStat, setOpenDialog, snackMessage }) {

  const dialogClick = (event) => {
    snackMessage("snackMessage")
    snackbarStat(true);
    setOpenDialog(false);
  };


  return (
    <div className="child">
      <button onClick={() => dialogClick()}>Click this dialog button</button>

      <p>
        {" "}
        Clicking the button should close dialog but why is it closing the
        snackbar also? <br />
        The snack bar opens for one second and disappears.
        <br />
        setOpenSnack("true") lines is useless when dialog is false.
      </p>

      
    </div>
  );
}

You have to place the Snackbar in the parent component because when placing it in child component, it will disappear whenever the child is unmounted

Moreover, if you want to set the message, just pass the setMsg to the child component and do the job

export default function Child({
  openDialog,
  setOpenDialog,
  setMsg,
  setOpenSnack
}) {
  const dialogClick = (event) => {
    setMsg("you closed from CHILD");
    setOpenSnack(true);
    setOpenDialog(false);
  };

  return (
    <div className="child">
      <button onClick={() => dialogClick()}>Click this dialog button</button>
      <p>
        ...
      </p>
    </div>
  );
}

Edit React Hooks Counter Demo

Related