Menu

Building a Global Notification System with React MUI Snackbar and Custom Hooks

As a front-end developer, creating a consistent notification system across your application is a common requirement. Users need feedback when they perform actions - whether successful or not. Material UI's Snackbar component offers an elegant solution for displaying temporary messages, but implementing it properly across a complex React application requires thoughtful architecture.

In this guide, I'll show you how to leverage MUI's Snackbar component to build a global notification system with a custom React hook. By the end, you'll have a reusable notification system that can be triggered from any component in your application without prop drilling or complex state management.

Learning Objectives

After reading this article, you'll be able to:

  • Understand MUI Snackbar's core functionality and configuration options
  • Build a global notification context to manage application-wide alerts
  • Create a custom hook for triggering notifications from any component
  • Implement different notification types (success, error, warning, info)
  • Customize the appearance and behavior of your notification system
  • Avoid common pitfalls when working with Snackbar components

MUI Snackbar Deep Dive

Before we start building our notification system, let's explore the Snackbar component in depth. Understanding its capabilities and limitations will help us design a better system.

What is a Snackbar?

Snackbars provide brief messages about app processes at the bottom of the screen. They're temporary and non-modal, meaning they don't interrupt the user experience. According to Material Design principles, snackbars should be used to provide feedback about an operation without blocking the main UI flow.

In MUI, the Snackbar component implements this pattern with a React component that handles its own positioning, timing, and animation states.

Core Props and Configuration

The Snackbar component comes with numerous props that control its behavior and appearance. Let's examine the most important ones:

PropTypeDefaultDescription
openbooleanfalseControls whether the Snackbar is displayed
autoHideDurationnumbernullThe number of milliseconds to wait before automatically closing
messagenode-The message to display
onClosefunction-Callback fired when the component requests to be closed
actionnode-The action to display, typically a Button component
anchorOriginobject vertical: 'bottom', horizontal: 'left' The position where the snackbar should appear
TransitionComponentcomponentGrowThe transition component to use
transitionDurationnumber | enter?: number, exit?: number -The duration for the transition in milliseconds

Basic Usage Pattern

The simplest implementation of a Snackbar looks like this:


import React, { useState } from 'react';
import { Button, Snackbar } from '@mui/material';

function BasicSnackbar() {
const [open, setOpen] = useState(false);

const handleClick = () => {
setOpen(true);
};

const handleClose = (event, reason) => {
if (reason === 'clickaway') {
return;
}
setOpen(false);
};

return (

<div>
  <Button onClick={handleClick}>Open Snackbar</Button>
  <Snackbar
    open={open}
    autoHideDuration={6000}
    onClose={handleClose}
    message="This is a basic snackbar message"
  />
</div>
); } 

In this basic example, the Snackbar is controlled by the open state. When the button is clicked, open becomes true, and the Snackbar appears. After 6 seconds (6000ms), the handleClose function is called automatically, setting open to false and hiding the Snackbar.

Snackbar vs. Alert

While Snackbar provides the positioning and timing mechanism, it doesn't include built-in styling for different message types (success, error, etc.). For that, MUI provides the Alert component, which can be nested inside a Snackbar:


import React, { useState } from 'react';
import { Button, Snackbar } from '@mui/material';
import MuiAlert from '@mui/material/Alert';

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

function AlertSnackbar() {
const [open, setOpen] = useState(false);

const handleClick = () => {
setOpen(true);
};

const handleClose = (event, reason) => {
if (reason === 'clickaway') {
return;
}
setOpen(false);
};

return (

<div>
  <Button onClick={handleClick}>Open Alert Snackbar</Button>
  <Snackbar open={open} autoHideDuration={6000} onClose={handleClose}>
    <Alert onClose={handleClose} severity="success">
      This is a success message!
    </Alert>
  </Snackbar>
</div>
); } 

The Alert component adds visual styling based on the severity prop, which can be "error", "warning", "info", or "success". This combination of Snackbar and Alert will be the foundation of our global notification system.

Customization Options

MUI's Snackbar offers several customization options:

  1. Positioning: Use the anchorOrigin prop to place the Snackbar anywhere on the screen.

<Snackbar
  anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
  open={open}
  message="Positioned in the top-right"
/>
  1. Transitions: Change how the Snackbar appears and disappears.

import { Slide } from '@mui/material';

function SlideTransition(props) {
return <Slide {...props} direction="up" />;
}

// Then in your component:

<Snackbar
  open={open}
  TransitionComponent={SlideTransition}
  message="Custom slide transition"
/>
  1. Duration: Control how long the Snackbar stays visible.

<Snackbar
  open={open}
  autoHideDuration={10000} // 10 seconds
  message="This stays visible longer"
/>
  1. Actions: Add interactive elements like buttons.

<Snackbar
  open={open}
  message="Message archived"
  action={
    <Button color="secondary" size="small" onClick={handleClose}>
      UNDO
    </Button>
  }
/>

Accessibility Considerations

Snackbars present some accessibility challenges:

  1. Duration: Users with cognitive disabilities may need more time to read messages. Consider extending the default duration.

  2. Screen readers: Ensure your message text is descriptive and meaningful for screen reader users.

  3. Color contrast: When customizing colors, maintain sufficient contrast for readability.

  4. Focus management: If your Snackbar contains interactive elements like buttons, ensure they receive focus appropriately.

MUI's implementation already handles many accessibility concerns, like ensuring the Snackbar is announced to screen readers. However, it's your responsibility to provide clear, concise messages and appropriate timing.

Building a Global Notification System

Now that we understand the Snackbar component, let's build a global notification system that can be accessed from anywhere in our application.

The Architecture

Our notification system will consist of:

  1. NotificationContext: A React context to manage notification state globally
  2. NotificationProvider: A provider component that renders the Snackbar
  3. useNotification: A custom hook to trigger notifications from any component

This approach follows the React Context API pattern, which allows components to share values without explicitly passing props through every level of the component tree.

Step 1: Create the Notification Context

First, let's create a context to manage our notification state:


// src/contexts/NotificationContext.js
import React, { createContext, useState, useContext } from 'react';

// Create a context with a default value
const NotificationContext = createContext({
open: false,
message: '',
severity: 'info',
showNotification: () => {},
hideNotification: () => {},
});

export const useNotificationContext = () => useContext(NotificationContext);

export const NotificationProvider = ({ children }) => {
  const [open, setOpen] = useState(false);
  const [message, setMessage] = useState('');
  const [severity, setSeverity] = useState('info');
  const [autoHideDuration, setAutoHideDuration] = useState(6000);

const showNotification = (message, severity = 'info', duration = 6000) => {
setMessage(message);
setSeverity(severity);
setAutoHideDuration(duration);
setOpen(true);
};

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

const value = {
open,
message,
severity,
autoHideDuration,
showNotification,
hideNotification,
};

return (

<NotificationContext.Provider value={value}>
  {children}
</NotificationContext.Provider>
); }; 

This context provides:

  • State for the notification (open, message, severity)
  • Functions to show and hide notifications
  • A custom hook (useNotificationContext) to access this context

Step 2: Create the Notification Component

Next, let's create a component that renders the Snackbar based on our context state:


// src/components/NotificationSystem.js
import React from 'react';
import { Snackbar, Alert } from '@mui/material';
import { useNotificationContext } from '../contexts/NotificationContext';

const NotificationSystem = () => {
const { open, message, severity, autoHideDuration, hideNotification } = useNotificationContext();

const handleClose = (event, reason) => {
if (reason === 'clickaway') {
return;
}
hideNotification();
};

return (

<Snackbar
  open={open}
  autoHideDuration={autoHideDuration}
  onClose={handleClose}
  anchorOrigin={{ vertical: "bottom", horizontal: "center" }}
>
  <Alert onClose={handleClose} severity={severity} sx={{ width: "100%" }}>
    {message}
  </Alert>
</Snackbar>
); };

export default NotificationSystem;

This component:

  • Uses our context to get the current notification state
  • Renders a Snackbar with an Alert inside it
  • Handles closing the notification

Step 3: Create the Custom Hook

Now, let's create a custom hook that makes it easy to trigger notifications from any component:


// src/hooks/useNotification.js
import { useNotificationContext } from '../contexts/NotificationContext';

const useNotification = () => {
const { showNotification } = useNotificationContext();

const notifySuccess = (message, duration) => {
showNotification(message, 'success', duration);
};

const notifyError = (message, duration) => {
showNotification(message, 'error', duration);
};

const notifyWarning = (message, duration) => {
showNotification(message, 'warning', duration);
};

const notifyInfo = (message, duration) => {
showNotification(message, 'info', duration);
};

return {
notifySuccess,
notifyError,
notifyWarning,
notifyInfo,
};
};

export default useNotification;

This hook provides convenient methods for different notification types, making it intuitive to use in components.

Step 4: Set Up the Provider in Your App

To make our notification system available throughout the app, we need to wrap our application with the NotificationProvider and include the NotificationSystem component:


// src/App.js
import React from 'react';
import { NotificationProvider } from './contexts/NotificationContext';
import NotificationSystem from './components/NotificationSystem';
import MainContent from './components/MainContent';

function App() {
return (

<NotificationProvider>
  <MainContent />
  <NotificationSystem />
</NotificationProvider>
); }

export default App;

Step 5: Use the Notification System in Components

Now we can use our notification system from any component in the application:


// src/components/SomeComponent.js
import React from 'react';
import { Button } from '@mui/material';
import useNotification from '../hooks/useNotification';

const SomeComponent = () => {
const { notifySuccess, notifyError, notifyInfo, notifyWarning } = useNotification();

const handleSuccessClick = () => {
notifySuccess('Operation completed successfully!');
};

const handleErrorClick = () => {
notifyError('An error occurred. Please try again.');
};

const handleInfoClick = () => {
notifyInfo('This is an informational message.');
};

const handleWarningClick = () => {
notifyWarning('Warning: This action cannot be undone.');
};

return (

<div>
  <h2>Notification Demo</h2>
  <Button variant="contained" color="success" onClick={handleSuccessClick}>
    Show Success
  </Button>
  <Button variant="contained" color="error" onClick={handleErrorClick}>
    Show Error
  </Button>
  <Button variant="contained" color="info" onClick={handleInfoClick}>
    Show Info
  </Button>
  <Button variant="contained" color="warning" onClick={handleWarningClick}>
    Show Warning
  </Button>
</div>
); };

export default SomeComponent;

Advanced Customization

Let's enhance our notification system with advanced features to make it more powerful and flexible.

Adding Support for Actions

Let's modify our system to support action buttons in notifications:


// Updated NotificationContext.js
import React, { createContext, useState, useContext } from 'react';

const NotificationContext = createContext({
open: false,
message: '',
severity: 'info',
action: null,
showNotification: () => {},
hideNotification: () => {},
});

export const useNotificationContext = () => useContext(NotificationContext);

export const NotificationProvider = ({ children }) => {
  const [open, setOpen] = useState(false);
  const [message, setMessage] = useState('');
  const [severity, setSeverity] = useState('info');
  const [autoHideDuration, setAutoHideDuration] = useState(6000);
  const [action, setAction] = useState(null);

const showNotification = (
message,
severity = 'info',
duration = 6000,
action = null
) => {
setMessage(message);
setSeverity(severity);
setAutoHideDuration(duration);
setAction(action);
setOpen(true);
};

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

const value = {
open,
message,
severity,
autoHideDuration,
action,
showNotification,
hideNotification,
};

return (

<NotificationContext.Provider value={value}>
  {children}
</NotificationContext.Provider>
); }; 

Then update the NotificationSystem component:


// Updated NotificationSystem.js
import React from 'react';
import { Snackbar, Alert, Button } from '@mui/material';
import { useNotificationContext } from '../contexts/NotificationContext';

const NotificationSystem = () => {
const {
open,
message,
severity,
autoHideDuration,
action,
hideNotification
} = useNotificationContext();

const handleClose = (event, reason) => {
if (reason === 'clickaway') {
return;
}
hideNotification();
};

return (

<Snackbar
  open={open}
  autoHideDuration={autoHideDuration}
  onClose={handleClose}
  anchorOrigin={{ vertical: "bottom", horizontal: "center" }}
>
  <Alert
    onClose={handleClose}
    severity={severity}
    sx={{ width: "100%" }}
    action={
      action && (
        <Button color="inherit" size="small" onClick={action.onClick}>
          {action.text}
        </Button>
      )
    }
  >
    {message}
  </Alert>
</Snackbar>
); };

export default NotificationSystem;

And finally, update the hook:


// Updated useNotification.js
import { useNotificationContext } from '../contexts/NotificationContext';

const useNotification = () => {
const { showNotification } = useNotificationContext();

const notifySuccess = (message, duration, action) => {
showNotification(message, 'success', duration, action);
};

const notifyError = (message, duration, action) => {
showNotification(message, 'error', duration, action);
};

const notifyWarning = (message, duration, action) => {
showNotification(message, 'warning', duration, action);
};

const notifyInfo = (message, duration, action) => {
showNotification(message, 'info', duration, action);
};

return {
notifySuccess,
notifyError,
notifyWarning,
notifyInfo,
};
};

export default useNotification;

Now we can use it with actions:


// Example usage with action
import React from 'react';
import { Button } from '@mui/material';
import useNotification from '../hooks/useNotification';

const UndoExample = () => {
const { notifySuccess } = useNotification();

const handleDelete = () => {
notifySuccess(
'Item deleted',
10000,
{
text: 'UNDO',
onClick: () => {
console.log('Undo delete action');
// Logic to undo the delete
}
}
);
};

return (

<Button variant="contained" color="primary" onClick={handleDelete}>
  Delete Item
</Button>
); };

export default UndoExample;

Supporting Multiple Notifications

One limitation of our current system is that it can only show one notification at a time. Let's enhance it to support multiple simultaneous notifications:


// MultipleNotificationsContext.js
import React, { createContext, useState, useContext } from 'react';
import { v4 as uuidv4 } from 'uuid';

const NotificationContext = createContext({
notifications: [],
addNotification: () => {},
removeNotification: () => {},
});

export const useNotificationContext = () => useContext(NotificationContext);

export const NotificationProvider = ({ children }) => {
  const [notifications, setNotifications] = useState([]);

const addNotification = (message, severity = 'info', duration = 6000, action = null) => {
const id = uuidv4();
const notification = {
id,
message,
severity,
duration,
action,
};

    setNotifications(prev => [...prev, notification]);

    // Auto-remove after duration
    if (duration !== null) {
      setTimeout(() => {
        removeNotification(id);
      }, duration);
    }

    return id;

};

const removeNotification = (id) => {
setNotifications(prev => prev.filter(notification => notification.id !== id));
};

const value = {
notifications,
addNotification,
removeNotification,
};

return (

<NotificationContext.Provider value={value}>
  {children}
</NotificationContext.Provider>
); }; 

Update the notification system to display multiple notifications:


// MultipleNotificationsSystem.js
import React from 'react';
import { Snackbar, Alert, Button } from '@mui/material';
import { useNotificationContext } from '../contexts/MultipleNotificationsContext';

const NotificationSystem = () => {
const { notifications, removeNotification } = useNotificationContext();

return (

<>
{notifications.map((notification) => (
<Snackbar
key={notification.id}
open={true}
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
sx={{
            // Stack notifications from bottom to top with 10px spacing
            bottom: `${10 + notifications.indexOf(notification) * 60}px`
          }} >
<Alert
severity={notification.severity}
sx={{ width: '100%' }}
onClose={() => removeNotification(notification.id)}
action={notification.action && (
<Button color="inherit" size="small" onClick={notification.action.onClick}>
{notification.action.text}
</Button>
)} >
{notification.message}
</Alert>
</Snackbar>
))}
</>
);
};

export default NotificationSystem;

And update the hook:


// useMultipleNotifications.js
import { useNotificationContext } from '../contexts/MultipleNotificationsContext';

const useNotification = () => {
const { addNotification, removeNotification } = useNotificationContext();

const notifySuccess = (message, duration, action) => {
return addNotification(message, 'success', duration, action);
};

const notifyError = (message, duration, action) => {
return addNotification(message, 'error', duration, action);
};

const notifyWarning = (message, duration, action) => {
return addNotification(message, 'warning', duration, action);
};

const notifyInfo = (message, duration, action) => {
return addNotification(message, 'info', duration, action);
};

const closeNotification = (id) => {
removeNotification(id);
};

return {
notifySuccess,
notifyError,
notifyWarning,
notifyInfo,
closeNotification,
};
};

export default useNotification;

Custom Notification Themes

Let's add support for custom theming of our notifications:


// CustomThemeNotificationSystem.js
import React from 'react';
import { Snackbar, Alert, Button, ThemeProvider, createTheme } from '@mui/material';
import { useNotificationContext } from '../contexts/NotificationContext';

// Define custom themes for different notification types
const successTheme = createTheme({
components: {
MuiAlert: {
styleOverrides: {
root: {
backgroundColor: '#2e7d32',
color: '#ffffff',
},
icon: {
color: '#ffffff',
},
},
},
},
});

const errorTheme = createTheme({
components: {
MuiAlert: {
styleOverrides: {
root: {
backgroundColor: '#d32f2f',
color: '#ffffff',
},
icon: {
color: '#ffffff',
},
},
},
},
});

const warningTheme = createTheme({
components: {
MuiAlert: {
styleOverrides: {
root: {
backgroundColor: '#ed6c02',
color: '#ffffff',
},
icon: {
color: '#ffffff',
},
},
},
},
});

const infoTheme = createTheme({
components: {
MuiAlert: {
styleOverrides: {
root: {
backgroundColor: '#0288d1',
color: '#ffffff',
},
icon: {
color: '#ffffff',
},
},
},
},
});

const getThemeForSeverity = (severity) => {
switch (severity) {
case 'success':
return successTheme;
case 'error':
return errorTheme;
case 'warning':
return warningTheme;
case 'info':
return infoTheme;
default:
return infoTheme;
}
};

const NotificationSystem = () => {
const { open, message, severity, autoHideDuration, action, hideNotification } = useNotificationContext();

const handleClose = (event, reason) => {
if (reason === 'clickaway') {
return;
}
hideNotification();
};

const theme = getThemeForSeverity(severity);

return (

<Snackbar
  open={open}
  autoHideDuration={autoHideDuration}
  onClose={handleClose}
  anchorOrigin={{ vertical: "bottom", horizontal: "center" }}
>
  <ThemeProvider theme={theme}>
    <Alert
      onClose={handleClose}
      severity={severity}
      sx={{ width: "100%" }}
      variant="filled"
      action={
        action && (
          <Button color="inherit" size="small" onClick={action.onClick}>
            {action.text}
          </Button>
        )
      }
    >
      {message}
    </Alert>
  </ThemeProvider>
</Snackbar>
); };

export default NotificationSystem;

Integration with API Calls

In real-world applications, notifications are often triggered by API responses. Let's create a utility to easily integrate our notification system with API calls:


// src/utils/apiWithNotification.js
import useNotification from '../hooks/useNotification';

export const useApiWithNotification = () => {
  const { notifySuccess, notifyError } = useNotification();

const fetchWithNotification = async (
apiCall,
{
successMessage = 'Operation successful',
errorMessage = 'An error occurred',
showSuccessNotification = true,
showErrorNotification = true,
} = {}
) => {
try {
const response = await apiCall();

      if (showSuccessNotification) {
        notifySuccess(successMessage);
      }

      return response;
    } catch (error) {
      if (showErrorNotification) {
        notifyError(errorMessage || error.message);
      }

      throw error;
    }

};

return { fetchWithNotification };
};

Now we can use this utility in our components:


// Example usage with API calls
import React, { useState } from 'react';
import { Button, CircularProgress } from '@mui/material';
import { useApiWithNotification } from '../utils/apiWithNotification';

const UserProfile = () => {
const [loading, setLoading] = useState(false);
const { fetchWithNotification } = useApiWithNotification();

const updateProfile = async () => {
setLoading(true);

    try {
      await fetchWithNotification(
        () => fetch('/api/profile', {
          method: 'PUT',
          body: JSON.stringify({ name: 'John Doe' }),
          headers: { 'Content-Type': 'application/json' }
        }),
        {
          successMessage: 'Profile updated successfully!',
          errorMessage: 'Failed to update profile. Please try again.'
        }
      );
    } finally {
      setLoading(false);
    }

};

return (

<Button variant="contained" onClick={updateProfile} disabled={loading}>
  {loading ? <CircularProgress size={24} /> : "Update Profile"}
</Button>
); };

export default UserProfile;

Best Practices and Common Issues

Let's cover some best practices and common issues when working with MUI Snackbars in a global notification system.

Performance Considerations

  1. Avoid Re-renders: Our context-based approach minimizes re-renders by isolating notification state from the rest of the application.

  2. Cleanup Timeouts: When using timeouts for auto-dismissal, ensure they are properly cleaned up to prevent memory leaks:


// Better implementation with cleanup
const addNotification = (message, severity = 'info', duration = 6000, action = null) => {
  const id = uuidv4();
  const notification = {
    id,
    message,
    severity,
    duration,
    action,
  };
  
  setNotifications(prev => [...prev, notification]);
  
  // Auto-remove after duration
  let timeoutId;
  if (duration !== null) {
    timeoutId = setTimeout(() => {
      removeNotification(id);
    }, duration);
  }
  
  // Store the timeout ID with the notification for cleanup
  notificationTimeouts.current[id] = timeoutId;
  
  return id;
};

const removeNotification = (id) => {
// Clear the timeout when removing a notification
if (notificationTimeouts.current[id]) {
clearTimeout(notificationTimeouts.current[id]);
delete notificationTimeouts.current[id];
}

setNotifications(prev => prev.filter(notification => notification.id !== id));
};

// Cleanup on unmount
useEffect(() => {
return () => {
// Clear all timeouts when the component unmounts
Object.values(notificationTimeouts.current).forEach(timeoutId => {
clearTimeout(timeoutId);
});
};
}, []);
  1. Limit Active Notifications: Too many notifications can overwhelm the UI. Consider limiting the number of active notifications:

const addNotification = (message, severity = 'info', duration = 6000, action = null) => {
  const id = uuidv4();
  const notification = {
    id,
    message,
    severity,
    duration,
    action,
  };
  
  // Limit to 3 notifications at a time, removing the oldest if needed
  setNotifications(prev => {
    if (prev.length >= 3) {
      const [oldest, ...rest] = prev;
      if (notificationTimeouts.current[oldest.id]) {
        clearTimeout(notificationTimeouts.current[oldest.id]);
        delete notificationTimeouts.current[oldest.id];
      }
      return [...rest, notification];
    }
    return [...prev, notification];
  });
  
  // Rest of the function...
};

Common Issues and Solutions

  1. Snackbar Disappearing Too Quickly

Problem: Users may miss notifications that disappear too quickly.

Solution: Adjust the duration based on message length:


const calculateDuration = (message) => {
  // Base duration of 4 seconds
  const baseDuration = 4000;
  // Add 100ms per character, capped at 10 seconds total
  const readingTime = Math.min(message.length * 100, 6000);
  return baseDuration + readingTime;
};

const notifySuccess = (message) => {
const duration = calculateDuration(message);
showNotification(message, 'success', duration);
};
  1. Z-Index Conflicts

Problem: Snackbars may appear behind other elements.

Solution: Ensure your Snackbar has a high enough z-index:


<Snackbar
  open={open}
  sx={{ zIndex: 9999 }}
  // other props...
>
  <Alert severity={severity}>{message}</Alert>
</Snackbar>
  1. Mobile Responsiveness

Problem: Snackbars may not be optimally positioned on mobile devices.

Solution: Adjust the positioning based on screen size:


import { useMediaQuery, useTheme } from '@mui/material';

const NotificationSystem = () => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));

// Other code...

return (

<Snackbar
open={open}
anchorOrigin={isMobile
? { vertical: 'bottom', horizontal: 'center' }
: { vertical: 'top', horizontal: 'right' }
}
// other props... >
<Alert severity={severity}>{message}</Alert>
</Snackbar>
);
};
  1. Handling Multiple Sequential Notifications

Problem: When multiple notifications are triggered in quick succession, they may stack and become unreadable.

Solution: Queue notifications and show them one after another:


// NotificationQueueContext.js
import React, { createContext, useState, useContext, useRef, useEffect } from 'react';

const NotificationContext = createContext({});

export const useNotificationContext = () => useContext(NotificationContext);

export const NotificationProvider = ({ children }) => {
  const [open, setOpen] = useState(false);
  const [message, setMessage] = useState('');
  const [severity, setSeverity] = useState('info');
  const [autoHideDuration, setAutoHideDuration] = useState(6000);
  const [action, setAction] = useState(null);
  
  const notificationQueue = useRef([]);
  const processingQueue = useRef(false);
  
  const processQueue = () => {
    if (notificationQueue.current.length === 0) {
      processingQueue.current = false;
      return;
    }
    
    processingQueue.current = true;
    const { message, severity, duration, action } = notificationQueue.current.shift();
    
    setMessage(message);
    setSeverity(severity);
    setAutoHideDuration(duration);
    setAction(action);
    setOpen(true);
  };
  
  const hideNotification = () => {
    setOpen(false);
    
    // Process next notification after a short delay
    setTimeout(() => {
      processQueue();
    }, 300);
  };
  
  const showNotification = (message, severity = 'info', duration = 6000, action = null) => {
    notificationQueue.current.push({ message, severity, duration, action });
    
    if (!processingQueue.current) {
      processQueue();
    }
  };
  
  const value = {
    open,
    message,
    severity,
    autoHideDuration,
    action,
    showNotification,
    hideNotification,
  };
  
  return (
    <NotificationContext.Provider value={value}>
      {children}
    </NotificationContext.Provider>
  );
};

Advanced Example: Notification System with Progress Tracking

For long-running operations, it can be useful to show progress in the notification. Let's implement this feature:


// ProgressNotificationContext.js
import React, { createContext, useState, useContext, useRef } from 'react';
import { v4 as uuidv4 } from 'uuid';

const NotificationContext = createContext({});

export const useNotificationContext = () => useContext(NotificationContext);

export const NotificationProvider = ({ children }) => {
  const [notifications, setNotifications] = useState([]);
  const timeoutRefs = useRef({});

const addNotification = (message, severity = 'info', duration = 6000, action = null) => {
const id = uuidv4();
const notification = {
id,
message,
severity,
duration,
action,
progress: null,
};

    setNotifications(prev => [...prev, notification]);

    if (duration !== null) {
      timeoutRefs.current[id] = setTimeout(() => {
        removeNotification(id);
      }, duration);
    }

    return id;

};

const updateNotificationProgress = (id, progress) => {
setNotifications(prev =>
prev.map(notification =>
notification.id === id
? { ...notification, progress }
: notification
)
);
};

const removeNotification = (id) => {
if (timeoutRefs.current[id]) {
clearTimeout(timeoutRefs.current[id]);
delete timeoutRefs.current[id];
}

    setNotifications(prev => prev.filter(notification => notification.id !== id));

};

const value = {
notifications,
addNotification,
updateNotificationProgress,
removeNotification,
};

return (

<NotificationContext.Provider value={value}>
  {children}
</NotificationContext.Provider>
); }; 

Create a component to display progress notifications:


// ProgressNotificationSystem.js
import React from 'react';
import { Snackbar, Alert, LinearProgress, Box, Button } from '@mui/material';
import { useNotificationContext } from './ProgressNotificationContext';

const NotificationSystem = () => {
const { notifications, removeNotification } = useNotificationContext();

return (

<>
{notifications.map((notification, index) => (
<Snackbar
key={notification.id}
open={true}
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
sx={{
            bottom: `${10 + index * 70}px`
          }} >
<Alert
severity={notification.severity}
sx={{ width: '100%' }}
onClose={() => removeNotification(notification.id)}
action={notification.action && (
<Button color="inherit" size="small" onClick={notification.action.onClick}>
{notification.action.text}
</Button>
)} >
<Box sx={{ width: '100%' }}>
{notification.message}
{notification.progress !== null && (
<LinearProgress
variant="determinate"
value={notification.progress}
sx={{ mt: 1 }}
/>
)}
</Box>
</Alert>
</Snackbar>
))}
</>
);
};

export default NotificationSystem;

Create a hook to use progress notifications:


// useProgressNotification.js
import { useNotificationContext } from './ProgressNotificationContext';

const useProgressNotification = () => {
const { addNotification, updateNotificationProgress, removeNotification } = useNotificationContext();

const startProgressNotification = (message, severity = 'info') => {
// Create a persistent notification (no auto-hide)
const id = addNotification(message, severity, null);

    // Set initial progress to 0
    updateNotificationProgress(id, 0);

    return {
      id,
      updateProgress: (progress) => {
        updateNotificationProgress(id, progress);
      },
      complete: (finalMessage) => {
        // Update message and make it auto-hide after 3 seconds
        removeNotification(id);
        addNotification(finalMessage || 'Operation completed', 'success', 3000);
      },
      error: (errorMessage) => {
        // Show error and make it auto-hide after 5 seconds
        removeNotification(id);
        addNotification(errorMessage || 'Operation failed', 'error', 5000);
      }
    };

};

return { startProgressNotification };
};

export default useProgressNotification;

Example usage for a file upload:


// FileUploadExample.js
import React, { useState } from 'react';
import { Button } from '@mui/material';
import useProgressNotification from './useProgressNotification';

const FileUploadExample = () => {
const [uploading, setUploading] = useState(false);
const { startProgressNotification } = useProgressNotification();

const simulateFileUpload = async (file) => {
// Create a progress notification
const notification = startProgressNotification('Uploading file...', 'info');

    setUploading(true);

    try {
      // Simulate upload progress
      for (let i = 0; i <= 100; i += 10) {
        notification.updateProgress(i);
        // Simulate network delay
        await new Promise(resolve => setTimeout(resolve, 500));
      }

      // Complete the notification
      notification.complete(`File "${file.name}" uploaded successfully!`);
    } catch (error) {
      // Show error notification
      notification.error(`Failed to upload "${file.name}": ${error.message}`);
    } finally {
      setUploading(false);
    }

};

const handleFileChange = (event) => {
const file = event.target.files[0];
if (file) {
simulateFileUpload(file);
}
};

return (

<Button variant="contained" component="label" disabled={uploading}>
  Upload File
  <input type="file" hidden onChange={handleFileChange} />
</Button>
); };

export default FileUploadExample;

Wrapping Up

We've built a comprehensive global notification system using MUI's Snackbar component and React hooks. This system provides a clean, reusable way to display notifications throughout your application without prop drilling or complex state management.

Our notification system offers:

  • Different notification types (success, error, warning, info)
  • Support for actions like "Undo"
  • Multiple simultaneous notifications
  • Custom theming options
  • Progress tracking for long-running operations
  • Integration with API calls

By leveraging React Context and custom hooks, we've created a solution that's both powerful and easy to use. This approach follows best practices for state management in React applications and provides a consistent user experience across your app.

Remember to consider accessibility when implementing notifications, ensuring that all users can perceive and interact with your notification system effectively. With the right configuration, your MUI Snackbar-based notification system will enhance the user experience without being intrusive or disruptive.