Creating an Alert System with Context and Hook in React
Tags: webdev, react, tutorial, ui
Published: 2023-07-14 11:58:00

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](https://github.com/tripathics/alerts-demo) or [demo](https://alerts-demo.netlify.app) for the alert system described in this post.

## Goals

1. 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.
2. We will create a wrapper for all alerts, `<AlertsWrapper />`. All the alerts within the app will stay inside this wrapper
3. 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.
4. `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.

```shell
npx create-react-app alerts-demo # create a react application
```

Change directory into the application folder. Open the project in your desired editor.

```shell
cd alerts-demo # change directory into the project
```

Start the development server.

```shell
npm start  # Start your application
```

### Tailwind CSS

Install [tailwind css](https://tailwindcss.com/docs/guides/create-react-app) and [tailwindcss/forms](https://github.com/tailwindlabs/tailwindcss-forms).

```shell
npm i -D tailwindcss @tailwindcss/forms
```

```shell
npx tailwindcss init
```

Open the `tailwind.config.js` file and update the file with the following content

```js
/** @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.

```css
@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.

```jsx
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

```jsx
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](localhost:3000) in a browser we can see our form.

![Form to add alerts](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f69npmt61eg2gsk9p1u4.png)

## 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](https://react.dev/learn/passing-data-deeply-with-context) and it's Provider to wrap the `App` 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 a `useAlerts` [hook](https://react.dev/learn/reusing-logic-with-custom-hooks) 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.

```js
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

```jsx
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`](https://react.dev/reference/react/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

![Styles for different severity of alerts](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nvk6lg8prwbj63rjjrbh.png)

> **Bonus:** We can also use our `<Alert />` component elsewhere in the app without providing `timeout` and/or `handleDismiss()` 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:

```jsx
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

```js
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

```jsx
// 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.

```jsx
// 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.

```jsx
// 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

```jsx
// 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.

![Our alerts system using the AlertsContext only](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ey5xv0v7odfkxugm9ngb.png)

To handle unmounting, we can create render `<AlertsTestForm />` conditionally with a state `showForm` in `<App />` component. **`App.js`** should finally look like this

```jsx
// 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;
```

_But..._
![Alerts not clearing as page unmounts](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xqbanoye4iij7hxhj23k.png)

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`](https://react.dev/reference/react/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.

```jsx
// 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.

```jsx
// 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's `addAlert()` 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](https://github.com/tripathics/alerts-demo).

> **PS:** I am still learning so I would love to hear some feedbacks and other approaches to solve this problem from my readers.