skip to Main Content

I am trying to extract the logic of the Material UI v5 SnackbarAlert into a re-usable component. I have found a very similar question answered recently, however my app is using JavaScript.

I’ve attempted to adapt this to JavaScript, but I am having issues with the component re-rendering multiple times upon open /close of the Alert Snackbar.

My code so far:

// src/AlertSnackbar.jsx
import React, { useEffect, useState } from 'react'
import Snackbar from '@mui/material/Snackbar';
import MuiAlert from '@mui/material/Alert';

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


export default function AlertSnackbar({message, ...otherProps}) {

  const [content, setContent] = useState(undefined);
  const [open, setOpen] = useState(false)
  const [pack, setPack] = useState([])

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

  //update content pack
  useEffect(()=> {
    message && setPack((prev) => [...prev, { message, key: new Date().getTime() }]);
  }, [message])

  //handle consecutive snackbars
  useEffect(() => {
    if (pack.length && !content) {
      //set a new snack when no active snack
      setContent({...pack[0]})
      setPack((prev)=> prev.slice(1))
      setOpen(true)
    } else if (pack.length && content && open) {
      //Close an active snack when a new one is added
      setOpen(false)
    }
  }, [pack, content, open])


  const handleExited = () => {
    setContent(undefined);
  };
  

  return (
    <Snackbar open={open} autoHideDuration={6000} onClose={handleClose} {...otherProps}
      TransitionProps={{ onExited: handleExited }} key={content?.key } 
    >
      <Alert onClose={handleClose} severity="success" sx={{ width: '100%' }}>
      <div>{content?.message}</div>
      </Alert>
    </Snackbar>
  )
}

Usage:

// src/SomeComponent.jsx
import React, { useState } from 'react'
import { Button } from '@mui/material'
import AlertSnackbar from '../components/AlertSnackbar'

export default SomeComponent = () => {

  const [snackContent, setSnackContent] = useState(<></>)

  const handleTestClick = () => setSnackContent(<>Hello, world!</>);


  return (
    <>
    <Button onClick={handleTestClick}>Test</Button>
    <AlertSnackbar message={snackContent} anchorOrigin={{ horizontal: "center", vertical: "bottom" }} />
    </>
  )
}

Any help would be much appreciated!

2

Answers


  1. Chosen as BEST ANSWER

    In the end, I could get the consecutive alert snackbar to work if I wrap the alert message with React.Fragment. Using just a string did not work.

    Triggering the snackbar on another page like so:

    //src/SomeComponent.jsx
    const [snackContent, setSnackContent] = useState("")
    
    const handleBtnOne= () => setSnackContent("Hello, world!"); //doesn't work
    const handleBtnTwo = () => setSnackContent(<>Goodbye World</>); //does work
    
    return (
    <>
    <Button onClick={handleBtnOne}>One</Button>
    <Button onClick={handleBtnTwo }>Two</Button>
    <AlertSnackbar message={snackContent} />
    </>
    )
    

    But I am unsure if using React.fragment snippets like this is valid?

    //src/AlertComponent.jsx
    import React, { useEffect, useState } from 'react'
    import Snackbar from '@mui/material/Snackbar';
    import MuiAlert from '@mui/material/Alert';
    
    
    const Alert = React.forwardRef(function Alert(props, ref) {
      return <MuiAlert elevation={6} ref={ref} variant="filled" {...props} />;
    });
    
    
    export default function AlertSnackbar({message, ...otherProps}) {
    
      const [content, setContent] = useState(undefined);
      const [open, setOpen] = useState(false)
      const [pack, setPack] = useState([])
    
      const handleClose = () => {
        setOpen(false);
        setContent(undefined)
      }
    
      //update content pack
      useEffect(()=> {
        message && setPack((prev) => [...prev, { message, key: new Date().getTime()}]);}, 
      [message])
    
    
      //handle consecutive snackbars
      useEffect(() => {
        if (pack.length && !content) {
          //set a new snack when no active snack
          setContent({...pack[0]})
          setPack((prev)=> prev.slice(1))
          setOpen(true)
        } else if (pack.length && content && open && pack[0].key!==content.key) {
          //Close an active snack when a new one is added
          setOpen(false)
        }
      }, [pack, content, open])
    
    
      const handleExited = () => {
        setContent(undefined);
      };
      
    
      return (
        <Snackbar open={open} autoHideDuration={6000} onClose={handleClose} 
          TransitionProps={{ onExited: handleExited }} key={content?.key } 
        >
          <Alert onClose={handleClose} severity="success" sx={{ width: '100%' }}>
          <div>{content?.message}</div>
          </Alert>
        </Snackbar>
      )
    

    1. I see is that you’re using a complex JSX element as the initial value for the snackContent state in SomeComponent. Instead, you should initialize it to an empty string or any other suitable initial value, like so:
    const [snackContent, setSnackContent] = useState('');
    
    1. You’re setting open to false when it’s called, but this doesn’t clear the content state. Instead, you should also set content to undefined when the Snackbar is closed, like so:
    const handleClose = () => {
          setOpen(false);
          setContent(undefined);
        };
    
    1. The condition if (pack.length && content && open) doesn’t cover the case where there are multiple items in the pack array. Instead, you should check if the pack array has changed, like so:
    useEffect(() => {
      if (pack.length && !content) {
        setContent({ ...pack[0] });
        setPack(prev => prev.slice(1));
        setOpen(true);
      } else if (pack.length && content && open && pack[0].key !== content.key) {
        setOpen(false);
      }
    }, [pack, content, open]);
    

    With these changes, your code should work correctly. Here’s the complete AlertSnackbar component:

    // src/AlertSnackbar.jsx
    import React, { useEffect, useState } from 'react';
    import Snackbar from '@mui/material/Snackbar';
    import MuiAlert from '@mui/material/Alert';
    
    const Alert = React.forwardRef(function Alert(props, ref) {
      return <MuiAlert elevation={6} ref={ref} variant="filled" {...props} />;
    });
    
    export default function AlertSnackbar({ message, ...otherProps }) {
      const [content, setContent] = useState(undefined);
      const [open, setOpen] = useState(false);
      const [pack, setPack] = useState([]);
    
      const handleClose = () => {
        setOpen(false);
        setContent(undefined);
      };
    
      useEffect(() => {
        message && setPack(prev => [...prev, { message, key: new Date().getTime() }]);
      }, [message]);
    
      useEffect(() => {
        if (pack.length && !content) {
          setContent({ ...pack[0] });
          setPack(prev => prev.slice(1));
          setOpen(true);
        } else if (pack.length && content && open && pack[0].key !== content.key) {
          setOpen(false);
        }
      }, [pack, content, open]);
    
      const handleExited = () => {
        setContent(undefined);
      };
    
      return (
        <Snackbar
          open={open}
          autoHideDuration={6000}
          onClose={handleClose}
          {...otherProps}
          TransitionProps={{ onExited: handleExited }}
          key={content?.key}
        >
          <Alert onClose={handleClose} severity="success" sx={{ width: '100%' }}>
            <div>{content?.message}</div>
          </Alert>
        </Snackbar>
      );
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search