DEV Community

Chandrashekhar Tripathi
Chandrashekhar Tripathi

Posted on

Creating an Alert System with Context and Hook in React

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

  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.

npx create-react-app alerts-demo # create a react application
Enter fullscreen mode Exit fullscreen mode

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

cd alerts-demo # change directory into the project
Enter fullscreen mode Exit fullscreen mode

Start the development server.

npm start  # Start your application
Enter fullscreen mode Exit fullscreen mode

Tailwind CSS

Install tailwind css and tailwindcss/forms.

npm i -D tailwindcss @tailwindcss/forms
Enter fullscreen mode Exit fullscreen mode
npx tailwindcss init
Enter fullscreen mode Exit fullscreen mode

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")],
};
Enter fullscreen mode Exit fullscreen mode

Add tailwind directives to the top of the index.css file.

@tailwind base;
@tailwind components;
@tailwind utilities;

body {
    margin: 0;
...
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

Now, if we go to localhost:3000 in a browser we can see our form.

Form to add alerts

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 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 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",
};
Enter fullscreen mode Exit fullscreen mode

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 };
Enter fullscreen mode Exit fullscreen mode

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

Styles for different severity of alerts

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:

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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));
  }

  ...

Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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>
  )

Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

The alertIds state array stores all the ids 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

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;
Enter fullscreen mode Exit fullscreen mode

But...
Alerts not clearing as page unmounts

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;

Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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.

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)