There’s something oddly satisfying about a toast notification popping up at just the right time — whether it’s a subtle “Saved successfully” or a sharp “Something went wrong,” these tiny messages quietly guide users through your app.
When I was building one of my React projects, I hit a point where I needed a global toast system — one that was fully in my control, could be triggered from anywhere, and wouldn’t become a dependency nightmare down the line.
Yes, there are fantastic libraries like react-toastify
. But as a
developer who likes to understand what’s under the hood, I decided to build my
own — using Redux Toolkit as the brain behind it.
Why Redux?
Because when you need consistent state management, global access, and predictable behavior — Redux shines.
In this tutorial, I’ll walk you through how I built a reusable toast/notification system using:
- React + Vite
- Redux Toolkit
- Tailwind CSS
You’ll learn how to:
- Set up Redux in a new project
- Design a toast UI that adapts to different message types
- Trigger toasts from anywhere in your app
- Auto-dismiss them after a few seconds
By the end, you’ll have a working toast system that’s easily extendable for your own apps.
📚 Table of Contents
- Project Setup: A Clean Slate with Vite
- Suggested File Structure
- Setting Up Redux: Store & Toast Slice
- Creating the Toast UI with Tailwind CSS
- Triggering Toasts in Your App
- Run It!
- Conclusion: Why This Matters
- Next Steps
Project Setup: A Clean Slate with Vite
I always prefer starting fresh, especially when building reusable components like a toast system. So here’s how I spun up the project from scratch.
Step 1: Create the Project Folder
mkdir redux-toast-demo
cd redux-toast-demo
Step 2: Scaffold a React App with Vite
npm create vite@latest . -- --template react
npm install
Step 3: Install Redux Toolkit + React-Redux
npm install @reduxjs/toolkit react-redux
Suggested File Structure
redux-toast-demo/
├── src/
│ ├── app/
│ │ └── store.js
│ ├── features/
│ │ └── toast/
│ │ └── toastSlice.js
│ ├── components/
│ │ └── Toast.jsx
│ ├── App.jsx
│ └── main.jsx
Setting Up Redux: Store & Toast Slice
Step 1: Create the Redux Store
src/app/store.js
import { configureStore } from '@reduxjs/toolkit';
import toastReducer from '../features/toast/toastSlice';
export const store = configureStore({
reducer: {
toast: toastReducer,
},
});
Step 2: Create the Toast Slice
src/features/toast/toastSlice.js
import { createSlice } from '@reduxjs/toolkit';
const initialState = {
toasts: [],
};
let nextId = 0;
const toastSlice = createSlice({
name: 'toast',
initialState,
reducers: {
addToast: {
reducer(state, action) {
state.toasts.push(action.payload);
},
prepare(message, type = 'info') {
return {
payload: {
id: nextId++,
message,
type,
},
};
},
},
removeToast(state, action) {
state.toasts = state.toasts.filter(t => t.id !== action.payload);
},
},
});
export const { addToast, removeToast } = toastSlice.actions;
export default toastSlice.reducer;
Step 3: Connect Redux to the App
src/main.jsx
import React, { StrictMode } from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { Provider } from 'react-redux';
import { store } from './app/store';
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
Creating the Toast UI with Tailwind CSS
Toast Component
src/components/Toast.jsx
import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { removeToast } from '../features/toast/toastSlice';
const Toast = () => {
const dispatch = useDispatch();
const toasts = useSelector((state) => state.toast.toasts);
useEffect(() => {
const timers = toasts.map((toast) =>
setTimeout(() => dispatch(removeToast(toast.id)), 3000)
);
return () => timers.forEach(clearTimeout);
}, [toasts, dispatch]);
return (
<div className="fixed top-5 right-5 space-y-2 z-50">
{toasts.map((toast) => (
<div
key={toast.id}
className={`px-4 py-2 rounded shadow-md text-white ${
toast.type === 'success'
? 'bg-green-500'
: toast.type === 'error'
? 'bg-red-500'
: 'bg-blue-500'
}`}
>
{toast.message}
</div>
))}
</div>
);
};
export default Toast;
Triggering Toasts in Your App
App.jsx
src/App.jsx
import React from 'react';
import Toast from './components/Toast';
import { useDispatch } from 'react-redux';
import { addToast } from './features/toast/toastSlice';
function App() {
const dispatch = useDispatch();
const handleShowToast = (type) => {
dispatch(addToast(`This is a ${type} message`, type));
};
return (
<div className="min-h-screen flex flex-col items-center justify-center gap-4">
<h1 className="text-2xl font-bold">Redux Toast Demo</h1>
<div className="flex gap-2">
<button
className="bg-blue-500 text-white px-4 py-2 rounded"
onClick={() => handleShowToast('info')}
>
Show Info
</button>
<button
className="bg-green-500 text-white px-4 py-2 rounded"
onClick={() => handleShowToast('success')}
>
Show Success
</button>
<button
className="bg-red-500 text-white px-4 py-2 rounded"
onClick={() => handleShowToast('error')}
>
Show Error
</button>
</div>
<Toast />
</div>
);
}
export default App;
Run It!
npm run dev
Open http://localhost:5173
and click any of the buttons. You’ll
see beautiful toast notifications pop in and auto-dismiss — exactly like you'd
expect in a modern app.
Conclusion: Why This Matters
By combining Redux Toolkit with React and Tailwind CSS, we now have a:
- Reusable, scalable toast notification system
- Globally accessible solution with Redux
- Auto-dismiss behavior without external libraries
If you’ve ever wanted more control over your app’s toast notifications, this is the perfect lightweight solution to start with.
Next Steps
- Add animations with
framer-motion
- Add dismiss (✕) buttons to manually close toasts
- Queue toasts to prevent overlap
- Support position (top-left, bottom-right, etc.)
Want the full code? Check out the GitHub Repo
Comments
Post a Comment