I have been learning React for a while now. Learning contexts made me realize that I could to make a simple alert system that could be used across the app.
Before I knew about contexts, I would just make Alert components and rendered them with any alert messages using State variables. But I wanted to make the alert seem more like in-app notifications that could be triggered, dismissed (or auto-dismissed if given a timeout) and had support for multiple alerts.
To achieve this, I used the Effect hook to start dismiss timers and clear the alert message but I would have to repeat this for every message and page where I used the Alert.
PREREQUISITES
- You must have basic knowledge of React.
WHAT YOU'LL NEED
- System with npm and node installed.
- An active internet connection.
View the source or demo for the alert system described in this post.
Goals
- We will create an
<Alert />
component with following features:- Different levels of severity like info, success, warning and error.
- Dismiss action button.
- Auto-dismiss according to severity level or according to
timeout
time.
- We will create a wrapper for all alerts,
<AlertsWrapper />
. All the alerts within the app will stay inside this wrapper - We will create an
AlertContext
:-
State
to store alerts, i.e. support for multiple alerts. -
addAlert()
function to add new alerts. -
removeAlert()
function to clear all alerts.
-
-
useAlerts
hook to access the alert system.
Setting up the project
Using create-react-app
create a new project. I am creating the project with project name alerts-demo
. You may choose a different name.
npx create-react-app alerts-demo # create a react application
Change directory into the application folder. Open the project in your desired editor.
cd alerts-demo # change directory into the project
Start the development server.
npm start # Start your application
Tailwind CSS
Install tailwind css and tailwindcss/forms.
npm i -D tailwindcss @tailwindcss/forms
npx tailwindcss init
Open the tailwind.config.js
file and update the file with the following content
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./src/**/*.{js,jsx,ts,tsx}"],
theme: {
extend: {},
},
plugins: [require("@tailwindcss/forms")],
};
Add tailwind directives to the top of the index.css
file.
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
margin: 0;
...
Creating a form for testing alerts
We will create a form to add new alerts in the file alert-test.js
in src/
folder.
import { useState } from "react";
export default function AlertTestForm() {
const initialFormData = { severity: "info", message: "", timeout: 0 };
const [formData, setFormData] = useState(initialFormData);
const handleChange = (e) => {
const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value }));
};
const handleSubmit = (e) => {
e.preventDefault();
let { severity, message, timeout } = formData;
// TODO: Add alert
setFormData(initialFormData);
};
return (
<form className="App-form" onSubmit={handleSubmit}>
<label
className="block text-sm font-medium text-gray-900 mt-6"
htmlFor="severity"
>
Alert severity
</label>
<select
className="w-full"
onChange={handleChange}
value={formData.severity}
required
name="severity"
id="severity"
>
<option value="info">Info</option>
<option value="success">Success</option>
<option value="warning">Warning</option>
<option value="error">Error</option>
</select>
<label
className="block text-sm font-medium text-gray-900 mt-6"
htmlFor="message"
>
Alert message
</label>
<input
onChange={handleChange}
value={formData.message}
className="w-full"
type="text"
name="message"
id="message"
required
placeholder="Alert message"
/>
<label
className="block text-sm font-medium text-gray-900 mt-6"
htmlFor="timeout"
>
Timeout
</label>
<input
onChange={handleChange}
value={formData.timeout}
min={0}
className="w-full"
type="number"
required
name="timeout"
id="timeout"
placeholder="Auto dismiss (in seconds)"
/>
<button
className="mt-6 w-full bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
type="submit"
>
Show alert
</button>
</form>
);
}
We then import <AlertTestForm />
inside the <App />
component. The App.js
file should look like this
import './App.css';
import './index.css';
import { useState } from 'react;
import AlertTestForm from './alert-test';
function App() {
return (
<AlertsProvider>
<div className="App">
<header className="App-header">
<h1>Alerts Demo</h1>
</header>
<main className="App-main p-4 m-auto max-w-sm min-w-fit w-full">
<AlertTestForm />
</main>
</div>
</AlertsProvider>
);
}
export default App;
Now, if we go to localhost:3000 in a browser we can see our form.
Creating files for our Alert system
Create a directory alerts/
under alerts-demo/src/
. In this directory create three files:
-
alert.js
: It will contain our<Alert />
and<AlertsWrapper />
components. -
alerts-context.js
: The context and it's Provider to wrap theApp
so that our alert system is accessible to any component in the tree below it. The alert system functions from the context will be returned through auseAlerts
hook which can be used in any component.
Create the <Alert />
component
First create severity-styles.js
in the alerts/
directory. We will export different styles for different severity level here. In this file we'll have:
classNames
-
svgPaths
: Paths for svg alert icons- info, success, warning & error. -
svgFillColors
: Fill colors for svg alert icons.
export const classNames = {
info: "bg-blue-100 border-blue-500 text-blue-700",
success: "bg-green-100 border-green-500 text-green-700",
warning: "bg-yellow-100 border-yellow-500 text-yellow-700",
error: "bg-red-100 border-red-500 text-red-700",
};
export const svgPaths = {
info: "M2.93 17.07A10 10 0 1 1 17.07 2.93 10 10 0 0 1 2.93 17.07zm12.73-1.41A8 8 0 1 0 4.34 4.34a8 8 0 0 0 11.32 11.32zM9 11V9h2v6H9v-4zm0-6h2v2H9V5z",
success: "M5.64 13.36l-2.28-2.28-1.28 1.28 3.56 3.56 7.72-7.72-1.28-1.28z",
warning:
"M10 4.5a1 1 0 0 1 2 0v5a1 1 0 1 1-2 0V4.5zm0 8a1 1 0 1 1 2 0v.5a1 1 0 1 1-2 0v-.5z",
error:
"M10 1C4.48 1 0 5.48 0 11s4.48 10 10 10 10-4.48 10-10S15.52 1 10 1zm1 15H9v-2h2v2zm0-4H9V5h2v7z",
};
export const svgFillColors = {
info: "text-blue-500",
success: "text-green-500",
warning: "text-yellow-500",
error: "text-red-500",
};
Open the alert.js
file and add the following content
import { useEffect } from "react";
import { classNames, svgFillColors, svgPaths } from "./severity-styles";
const Alert = ({
message = "",
severity = "info",
timeout = 0,
handleDismiss = null,
}) => {
useEffect(() => {
if (timeout > 0 && handleDismiss) {
const timer = setTimeout(() => {
handleDismiss();
}, timeout * 1000);
return () => clearTimeout(timer);
}
}, []);
return (
message?.length && (
<div
className={
classNames[severity] +
" rounded-b px-4 py-3 mb-4 shadow-md pointer-events-auto"
}
role="alert"
>
<div className="flex">
<div className="py-1">
<svg
className={"fill-current h-6 w-6 mr-4 " + svgFillColors[severity]}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
>
<path d={svgPaths[severity]} />
</svg>
</div>
<div>
<p className="font-bold">{severity.toUpperCase()}</p>
<p className="text-sm">{message}</p>
</div>
<div className="ml-auto">
{handleDismiss && (
<button
className="text-sm font-bold"
type="button"
onClick={(e) => {
e.preventDefault();
handleDismiss();
}}
>
<svg
className="fill-current h-6 w-6 text-gray-500"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
>
<path d="M6.83 5L10 8.17 13.17 5 15 6.83 11.83 10 15 13.17 13.17 15 10 11.83 6.83 15 5 13.17 8.17 10 5 6.83 6.83 5z" />
</svg>
</button>
)}
</div>
</div>
</div>
)
);
};
const AlertsWrapper = ({ children }) => {
return (
<div className="fixed top-0 right-0 p-4 z-50 pointer-events-none max-w-sm min-w-fit w-full">
{children}
</div>
);
};
export { Alert, AlertsWrapper };
Here useEffect
hook is used for setting a timer to dismiss the alert if timeout
prop is greater than 0
and handleDismiss()
prop is not null. Also if handleDismiss()
is not defined, the dismiss button will not be available.
The <AlertsWrapper />
component will contain all our alerts. We can style it so that we show the alerts at the desired position on the screen. I am showing the alerts at the top-right corner of the screen.
This is what our alerts will look like
Bonus: We can also use our
<Alert />
component elsewhere in the app without providingtimeout
and/orhandleDismiss()
props, like using it to display some important message on a page that does not require dismissing. It fills the width of parent element.
The Alerts Context
Now that our <Alert />
component is ready, let's create the alerts context to manage alerts.
Open the alerts-context.js
file. Let's create a context first:
import { createContext, useState } from "react";
import { Alert, AlertsWrapper } from "./alert";
const AlertsContext = createContext();
const AlertsProvider = ({ children }) => {
const [alerts, setAlerts] = useState([]);
return (
<AlertsContext.Provider>
<AlertsWrapper>
{alerts.map((alert) => (
<Alert key={alert.id} {...alert} handleDismiss={() => {}} />
))}
</AlertsWrapper>
{children}
</AlertsContext.Provider>
);
};
The alerts
state is used to store an array of alerts. Each alert is given by
alert = {
id: String,
severity: 'info' | 'success' | 'warning' | 'error',
message: String,
timeout: Number,
handleDismiss: function
}
Now we have to create functions for adding and dismissing alerts
// alerts-context.js
...
const AlertsProvider = ({ children }) => {
...
const addAlert = (alert) => {
const id = Math.random().toString(36).slice(2, 9) + new Date().getTime().toString(36);
setAlerts((prev) => [{ ...alert, id: id }, ...prev]);
return id;
}
const dismissAlert = (id) => {
setAlerts((prev) => prev.filter((alert) => alert.id !== id));
}
...
In the addAlert()
function, we are taking alert
-> ({message, severity, timeout}
) as a parameter. We generate a random id
string and add the alert along with the id, { id, message, severity, timeout }
to the top of alerts
state array.
Then we return the id
so that the component that added this alert can later dismiss it.
dismissAlert()
is a simple function that takes the alert's id
and removes that alert from alerts
state array which has that id
.
Now we pass alerts
, addAlert()
and dismissAlert()
as values
to the <AlertsProvider />
and export it.
// alerts-context.js
import { createContext, useState } from "react";
import { Alert, AlertsWrapper } from "./alert";
const AlertsContext = createContext();
const AlertsProvider = ({ children }) => {
...
const addAlert(alert) => {
...
}
const dismissAlert(id) => {
...
}
return (
<AlertsContext.Provider value={{ alerts, addAlert, dismissAlert }}>
<AlertsWrapper>
{alerts.map((alert) => (
<Alert key={alert.id} {...alert} handleDismiss={() => { dismissAlert(alert.id) }} />
))}
</AlertsWrapper>
{children}
</AlertsContext.Provider>
)
}
export default AlertsProvider;
We now import <AlertsProvider />
in App.js
and wrap it around the component.
// App.js
import './App.css';
import './index.css';
import { useState } from 'react';
import AlertsProvider from './alerts/alerts-context';
function App() {
const initialFormData = { severity: 'info', message: '', timeout: 0 };
const [formData, setFormData] = useState(initialFormData);
...
return (
<AlertsProvider>
<div className="App">
...
</div>
</AlertsProvider>
)
Using the AlertsContext
Our alert system is now accessible from our <AlertTestForm />
component. We can use the AlertsContext
with the useContext
hook and check how it is working. Modifying alert-test.js
file use addAlert()
from AlertsContext
. The final file should look like this
// alerts-test.js
import { useContext, useState } from "react";
import { AlertsContext } from "./alerts/alerts-context";
export default function AlertTestForm() {
const initialFormData = { severity: "info", message: "", timeout: 0 };
const [formData, setFormData] = useState(initialFormData);
const [alertIds, setAlertIds] = useState([]);
const { addAlert, dismissAlert } = useContext(AlertsContext);
const handleChange = (e) => {
const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value }));
};
// clear all alerts added by this component
const clearAlerts = () => {
alertIds.forEach((id) => {
dismissAlert(id);
});
setAlertIds([]);
};
const handleSubmit = (e) => {
e.preventDefault();
let { severity, message, timeout } = formData;
let id = addAlert({ severity, message, timeout });
setAlertIds((prev) => [...prev, id]);
setFormData(initialFormData);
};
useEffect(() => {
return () => {
clearAlerts();
};
}, []);
return (
<form className="App-form" onSubmit={handleSubmit}>
<label
className="block text-sm font-medium text-gray-900 mt-6"
htmlFor="severity"
>
Alert severity
</label>
<select
className="w-full"
onChange={handleChange}
value={formData.severity}
required
name="severity"
id="severity"
>
<option value="info">Info</option>
<option value="success">Success</option>
<option value="warning">Warning</option>
<option value="error">Error</option>
</select>
<label
className="block text-sm font-medium text-gray-900 mt-6"
htmlFor="message"
>
Alert message
</label>
<input
onChange={handleChange}
value={formData.message}
className="w-full"
type="text"
name="message"
id="message"
required
placeholder="Alert message"
/>
<label
className="block text-sm font-medium text-gray-900 mt-6"
htmlFor="timeout"
>
Timeout
</label>
<input
onChange={handleChange}
value={formData.timeout}
min={0}
className="w-full"
type="number"
required
name="timeout"
id="timeout"
placeholder="Auto dismiss (in seconds)"
/>
<button
className="mt-6 w-full bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
type="submit"
>
Show alert
</button>
<button
className="mt-6 w-full bg-gray-200 hover:bg-gray-300 text-gray-900 py-2 px-4 rounded"
type="button"
onClick={clearAlerts}
>
Clear alerts
</button>
</form>
);
}
The alertIds
state array stores all the id
s of alerts generated by our <AlertTestForm />
component. In case we want to clear all alerts, we can use clearAlerts()
function. clearAlerts()
will also be called when the component unmounts (through the useEffect
hook) in <AlertsTestForm />
component.
To handle unmounting, we can create render <AlertsTestForm />
conditionally with a state showForm
in <App />
component. App.js
should finally look like this
// App.js
import "./App.css";
import "./index.css";
import { useState } from "react";
import AlertsProvider from "./alerts/alerts-context";
import AlertTestForm from "./alert-test";
function App() {
const [showForm, setShowForm] = useState(true);
return (
<AlertsProvider>
<div className="App">
<header className="App-header">
<h1>Alerts Demo</h1>
</header>
<main className="App-main p-4 m-auto max-w-sm min-w-fit w-full">
<button
onClick={() => setShowForm((prev) => !prev)}
className="mt-6 w-fit bg-gray-200 border-gray-700 border-2 hover:bg-gray-300 text-gray-900 font-bold py-2 px-4 rounded"
type="button"
>
{showForm ? "Unmount " : "Mount "} form
</button>
{showForm && <AlertTestForm />}
</main>
</div>
</AlertsProvider>
);
}
export default App;
The alerts are not getting cleared. This is because when the <AlertTestForm />
the states also get lost. Thus there is nothing to "clear" and the alerts are not getting cleared. We can use the useRef
hook to keep track of the alert ids that are added by this component.
The system of alerts we're making is supposed to be used by many pages in the app. Instead of declaring these state variables for alertIds
, references for not losing these alertIds
, functions to clearAlerts()
and handling clearing of alerts in the component's cleanup function, we can create a custom hook.
The useAlerts
hook
Create the useAlerts
hook at the bottom of alerts-context.js
file.
// alerts-context.js
import { createContext, useContext, useRef, useState } from "react";
import { Alert, AlertsWrapper } from "./alert";
const AlertsContext = createContext();
...
export const useAlerts = () => {
const [alertIds, setAlertIds] = useState([]);
const alertIdsRef = useRef(alertIds);
const { addAlert, dismissAlert } = useContext(AlertsContext);
const addAlertWithId = (alert) => {
const id = addAlert(alert);
alertIdsRef.current.push(id);
setAlertIds(alertIdsRef.current);
}
const clearAlerts = () => {
alertIdsRef.current.forEach((id) => dismissAlert(id));
alertIdsRef.current = [];
setAlertIds([]);
}
return { addAlert: addAlertWithId, clearAlerts };
}
export default AlertsProvider;
Also we don't need to export AlertsContext
.
Refactoring alerts-test.js
to use useAlerts
instead of useContext
and AlertsContext
, the file should finally look like this.
// alerts-test.js
import { useEffect, useState } from "react";
import { useAlerts } from "./alerts/alerts-context";
export default function AlertTestForm() {
const initialFormData = { severity: "info", message: "", timeout: 0 };
const [formData, setFormData] = useState(initialFormData);
const { addAlert, clearAlerts } = useAlerts();
const handleChange = (e) => {
const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value }));
};
const handleSubmit = (e) => {
e.preventDefault();
let { severity, message, timeout } = formData;
addAlert({ severity, message, timeout });
setFormData(initialFormData);
};
// clear alerts when this component unmounts.
useEffect(() => {
return () => {
clearAlerts();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<form className="App-form" onSubmit={handleSubmit}>
<label
className="block text-sm font-medium text-gray-900 mt-6"
htmlFor="severity"
>
Alert severity
</label>
<select
className="w-full"
onChange={handleChange}
value={formData.severity}
required
name="severity"
id="severity"
>
<option value="info">Info</option>
<option value="success">Success</option>
<option value="warning">Warning</option>
<option value="error">Error</option>
</select>
<label
className="block text-sm font-medium text-gray-900 mt-6"
htmlFor="message"
>
Alert message
</label>
<input
onChange={handleChange}
value={formData.message}
className="w-full"
type="text"
name="message"
id="message"
required
placeholder="Alert message"
/>
<label
className="block text-sm font-medium text-gray-900 mt-6"
htmlFor="timeout"
>
Timeout
</label>
<input
onChange={handleChange}
value={formData.timeout}
min={0}
className="w-full"
type="number"
required
name="timeout"
id="timeout"
placeholder="Auto dismiss (in seconds)"
/>
<button
className="mt-6 w-full bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
type="submit"
>
Show alert
</button>
</form>
);
}
Of course it's up to you if you don't want to clear alerts upon component unmount, you can remove that cleanup function in the effect hook that calls clearAlerts()
. This way alerts triggered by a page will stay visible even if you switch to a different page.
Conclusion
We have made an Alert system where:
- One can easily trigger alerts by using the
useAlerts
hook'saddAlert()
function from any component below the<AlertsProvider />
. - The system supports showing multiple alerts at a time with features like auto-dismiss with timer.
-
clearAlerts()
function can be used clear alerts created by a component. It which can be used to clear alerts as a component (like a page) unmounts.
So this is how we create an Alert system in React using Contexts and Hooks. Get the full code for this alert-system in my tripathics/alerts-demo github repository.
PS: I am still learning so I would love to hear some feedbacks and other approaches to solve this problem from my readers.
Top comments (0)