Spread operator not working with setState (React, Typescript & Material-ui)

Viewed 1532

I made a custom Snackbar component in React using Material-ui (and Typescript). I have trouble understanding how the spread operator and await work. (Example here: https://codesandbox.io/s/gifted-hopper-tkkpi?file=/src/App.tsx:1311-1380)

Full code:

import React, { FunctionComponent, useEffect } from "react";
import Button from "@material-ui/core/Button";
import Snackbar from "@material-ui/core/Snackbar";
import { Color } from "@material-ui/lab";
import { Alert } from "@material-ui/lab";

interface SnackbarModel {
  open: boolean;
  severity?: Color;
  message: string;
}

const CustomSnackbar: FunctionComponent<SnackbarModel> = (props) => {
  const [opened, setOpened] = React.useState(false);

  useEffect(() => {
    if (props.open) {
      setOpened(true);
    }
  }, [props.open]);

  const handleClose = (event: React.ChangeEvent<object>, reason: string) => {
    if (reason === "clickaway") {
      return;
    }
    setOpened(false);
  };

  return (
    <div>
      <Snackbar
        open={opened}
        autoHideDuration={2000}
        onClose={handleClose}
        anchorOrigin={{ vertical: "top", horizontal: "center" }}
      >
        <Alert severity={props.severity}>{props.message}</Alert>
      </Snackbar>
    </div>
  );
};

export default function App() {
  const [snackbar, setSnackbar] = React.useState<SnackbarModel>({
    open: false,
    message: ""
  });

  async function handleClick() {
    
    await fetch(Stuff...)
    // Working as desired
    //setSnackbar({open: true, severity: "success", message: "Snackbar"});
    //setSnackbar({open: false, severity: "success", message: "Snackbar"});

    // Somehow not working
    setSnackbar({ open: true, severity: "success", message: "Snackbar" });
    setSnackbar({ ...snackbar, open: false });
  }

  return (
    <div>
      <CustomSnackbar
        open={snackbar.open}
        message={snackbar.message}
        severity={snackbar.severity}
      />

      <Button variant="contained" onClick={handleClick}>
        Snackbar
      </Button>
    </div>
  );
}

I trigger the Snackbar to open with

const [snackbar, setSnackbar] = React.useState<SnackbarModel>({
   open: false,
   message: ""
});


async function handleClick() {
   
   await fetch(Stuff)
   setSnackbar({open: true, severity: "success", message: "Snackbar"}) 
   setSnackbar({open: false, severity: "success", message: "Snackbar"});  
}

The Snackbar automatically closes after a set amount of Time. "open" only triggers the Snackbar and doesn't control the setOpen-state of the Snackbar.

Now I tried:

setSnackbar({ open: true, severity: "success", message: "Snackbar" });
console.log(snackbar); //gives you: Object {open: false, message: ""}
setSnackbar({ ...snackbar, open: false });  

This successfully triggers the Snackbar but the message/text is lost.
Question 1: Shouldn't "...snackbar" still have "message: 'Snackbar' " saved?

For some reason i also have to make the function async and await the first "setSnackbar" if I don't use "await fetch(Stuff...):

async function handleClick() {

  //working as desired
  await setSnackbar({open: true, severity: "success", message: "Snackbar"});
  setSnackbar({open: false, severity: "success", message: "Snackbar"});

  // OR

  //not working as desired
  await setSnackbar({ open: true, severity: "success", message: "Snackbar" });
  setSnackbar({ ...snackbar, open: false });
}

Altough the await on "setSnackbar" is apparently not needed if you first have another line of code you await. In my use case this works:

async function handleClick() {
  
  await fetch(Stuff...)
  setSnackbar({open: true, severity: "success", message: "Snackbar"});
  setSnackbar({open: false, severity: "success", message: "Snackbar"});

}

Question 2: Can someone explain why the await is required/not required?

1 Answers

The main problem in your implementation is that setSnackbar is handled as if it is synchronous, but it "kinda" isn't. It is thoroughly described here why: useState set method not reflecting change immediately (especially the second answer)

Answer 1:

The spread operator doesn't work, because most of the times, the first setSnackbar is not completed.

In order to fix this, you should either use the useEffect method passing the open attribute of the snackback object as a dependency in the array.

You should do the same thing as you did in your CustomSnackbar:

useEffect(() => {
  if (snackbar.open) {
    setSnackbar((_snackbar) => ({ ..._snackbar, open: false }));
  }
}, [snackbar.open]);

Notice the _snackbar, it is used used in the callback of setSnackbar to get the at-that-time value of the open attribute.

Answer 2: About the await.

You need the await for api calls because api calls are asynchronous/Promises, meaning that the program will execute the next command before they are completed, thus you need to "await" for them, or use .then() to do stuff when they are finished.

The setSnackbar is not a promise, but yet it does not take effect immediately, that is why React has callbacks for setState and useEffect. To handle such cases. So, using await before the setSnackbar makes not difference

Related