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:
Prop | Type | Default | Description |
---|---|---|---|
open | boolean | false | Controls whether the Snackbar is displayed |
autoHideDuration | number | null | The number of milliseconds to wait before automatically closing |
message | node | - | The message to display |
onClose | function | - | Callback fired when the component requests to be closed |
action | node | - | The action to display, typically a Button component |
anchorOrigin | object | vertical: 'bottom', horizontal: 'left' | The position where the snackbar should appear |
TransitionComponent | component | Grow | The transition component to use |
transitionDuration | number | 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:
- 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"
/>
- 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"
/>
- Duration: Control how long the Snackbar stays visible.
<Snackbar
open={open}
autoHideDuration={10000} // 10 seconds
message="This stays visible longer"
/>
- 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:
-
Duration: Users with cognitive disabilities may need more time to read messages. Consider extending the default duration.
-
Screen readers: Ensure your message text is descriptive and meaningful for screen reader users.
-
Color contrast: When customizing colors, maintain sufficient contrast for readability.
-
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:
- NotificationContext: A React context to manage notification state globally
- NotificationProvider: A provider component that renders the Snackbar
- 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
-
Avoid Re-renders: Our context-based approach minimizes re-renders by isolating notification state from the rest of the application.
-
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);
});
};
}, []);
- 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
- 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);
};
- 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>
- 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>
);
};
- 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.