Building a notification system

25 August, 20198 min to read

Today I want to talk about how you can build a very minimal notification system for your React app. The system that we are gonna build will be relying heavily on two important concepts in React: context & hooks. This article assumes you have a basic understanding about these concepts, but if you're new to them I recommend to checkout the official docs to get some sense on whats going on. As far as styling and animation are concerned, I will be using styled-components and Framer's fresh animation lib framer-motion.

By the way, this is the first blog post I have ever written. So, don't be to hard on me 😉. Anyway, hope you guys like it!

What are we going to build?

Requirements

First, let's take a look at what we want to build. We want...

  • the notification to have a title and description
  • three notification types: success / warning / error
  • the notification to show a different icon / color based on the type of notification
  • to trigger a notification from anywhere within our app, ideally by calling a function
  • the notification to automatically disappear after x seconds
  • the user to be able to close the notification (before it disappears automatically)
  • to optionally allow the user to click an action-button (ie. to see more details)
  • a nice in and out animation

Notification component

The actual <Notification/> is not that interesting to be honest. As a matter of fact, I'm sure you can come up with a much nicer variant yourself. If you like you can checkout the attached sandbox for the source. But for now, let's focus on it's props, which are important:

<Notification
  type="success" // or 'warning' | 'error'
  title="Title" // title of the notification
  description="Description" // description of the notification
  // allows us to close the notification when the user clicks 'Close'
  onClose={() => console.log("close")}
  // optional, allows us to respond when the user clicks 'More...'
  onMore={() => console.log("more")}
/>

This is what it will look like:

bla

Notification mechanism

So we want to trigger a notification from anywhere within our app. How are we going to achieve that?

Well, the nice thing about Context, is that it allows a component to consume state and behavior from a thing called a provider somewhere higher up the tree. In our case, that's precisely what we need. We want a 'thing' higher up in our app that handles all the notification logic, and a way for components that are interested to shout upwards "Hey Provider, I like you to show me a notification!". Let me illustrate this with some pseudo-code:

<Provider notify={notify}>
  // some levels deeper in the app....
  <Consumer>
    {notify => <button onClick={notify}>Trigger notification</button>}
  </Consumer>
</Provider>

So let's start with creating the context...

// NotificationProvider.js

import * as React from "react";

const NotificationContext = React.createContext();

Now that we got the context, let's move on with building the Provider...

The Provider

The provider is responsible for a couple of things:

  • It should keep track of which notifications to show or to hide
  • It should control the presentation (partially) -> layout / animation
  • It should expose a function to trigger a notification and provide it through context

We begin by declaring a place to store the notifications

// NotificationProvider.js

function NotificationProvider({ children }) {
  const [notifications, setNotifications] = React.useState([]);

  // ...
}

Next we need a way to add a notification:

// NotificationProvider.js

import * as React from "react";
import shortId from "shortId"; // small lib for generating id's

// ... skipped for brevity ...

function NotificationProvider({ children }) {
  const [notifications, setNotifications] = React.useState([]);

  const notify = React.useCallback(
    ({ type, title, description, onMore /* optional */ }) => {
      // assign a new generated id
      const id = shortId();

      // eventually, we want to remove the notification we are about to add
      // - when the user clicks 'Close' (hence 'onClose')
      // - after 2000ms
      function removeNotification() {
        setNotifications(notifications =>
          notifications.filter(n => n.id !== id)
        );
      }

      // create new notification
      const newNotification = {
        id,
        type,
        title,
        description,
        onMore,
        onClose: removeNotification,
      };

      // add it to the state
      setNotifications(notifications => [...notifications, newNotification]);

      // register deletion of the notification after 2000ms
      setTimeout(removeNotification, 2000);
    },
    []
  );

  return (
    <NotificationContext.Provider value={notify}>
      {children}
    </NotificationContext.Provider>
  );
}

Perhaps there are a few things that need some explanation.

You might wonder why we are wrapping notify in React.useCallback. I'm afraid an in-depth explanation is unfortunately out of this article's scope, but to put it simply, it might prevent some unnecessary renders on components that rely on context (NotificationContext on our case). I suggest you read this excellent article by Kent C. Dodds to gain some more insight.

Also notice how we're passing a function to setNotifications. Since notify is wrapped in a useCallback with an empty dependency-array, it's inner scope refers to variables outside notify at the time it was created. So without using an update-function, notifications would always be an empty array. Fortunately, by passing a function to setNotifications we get an up to date version of the state as first argument 🙂.

As a finishing touch I like to move the code we've just discussed to it's own custom hook. Now, I hear you think: 'Man... is that really necessary?'. No, it's not... just a matter of taste I guess. But let me try to explain my thought process nonetheless. I'm a lazy code-reader (no, not(!) lazy in general silly 😉). I like to read and understand the essence of a function as quickly as possible, and having to 'decipher' 10 lines of code in order to see what it is doing as a whole does not help in that regard. Let's imagine these 'standard' hooks were molecules, working together and forming some kind of creepy organism. Wouldn't it be nice if we could name this creepy organism after it's purpose (=== free summary)?

Ok enough... here's the custom hook:

// NotificationProvider.js

function useNotifications() {
  const [notifications, setNotifications] = React.useState([]);

  const notify = React.useCallback(({ type, title, description, onMore }) => {
    const id = shortId();

    function removeNotification() {
      setNotifications(notifications => notifications.filter(n => n.id !== id));
    }

    const newNotification = {
      id,
      type,
      title,
      description,
      onMore,
      onClose: removeNotification,
    };

    setNotifications(notifications => [...notifications, newNotification]);

    setTimeout(removeNotification, 2000);
  }, []);

  return { notify, notifications };
}

Next, we need a place to show the notifications...

useCreateDomElement

We want our notifications to be placed in a layer above our app, and ideally in a way that this notification-layer does not interfere with other elements in our app. For such layer-specific things, I like to use portals. From to official React docs:

Portals provide a first-class way to render children into a DOM node that exists outside the DOM hierarchy of the parent component.

Sounds cool right?

The thing is, React portals need a reference to a dom-element in order to render. So let's create a custom hook that does just that:

// NotificationProvider.js

function useDomElement() {
  const [domElement, setDomElement] = React.useState(null);

  /**
   * On mount, create a div-element, append it to the body and
   * save a reference in state
   */
  React.useEffect(() => {
    const element = document.createElement("div");
    document.body.appendChild(element);
    setDomElement(element);

    return () => document.body.removeChild(element);
  }, []);

  return domElement;
}

Back to the NotificationProvider

We only need some kind of container to place our notifications in:

const NotificationsContainer = styled.div`
  position: fixed;
  top: 16px;
  right: 16px;
  // in case of multiple notifications we want to make
  // sure we we can click 'through' the area between the
  // notifications
  pointer-events: none;
`;

By now, we have a nice small component with a clear overview on what's going on exactly. Let me show you a brief summary:

function NotificationProvider({ children }) {
  // our custom hooks which returns a reference to a dom-element
  const notificationRoot = useDomElement();

  // our notify function to add a notification, and
  // the data of the current notifications
  const { notify, notifications } = useNotifications();

  return (
    <>
      {/* We are making our notify function available through context for other
          components to use */}
      <NotifyContext.Provider value={notify}>{children}</NotifyContext.Provider>

      {/*  Once we've gotten the dom-element we can render the actual
           notifications */}
      {notificationRoot &&
        React.createPortal(
          <NotificationsContainer>
            {notifications.map(notification => (
              {/* The data structure of our state matches the props of
                  <Notification />. So we can just destructure them as props  */}
              <Notification key={notification.id} {...notification} />
            ))}
          </NotificationsContainer>,
          notificationRoot
        )}
    </>
  );
}

framer-motion

The notifications are working fine so far, although they don't feel quite right. Animations to the rescue!

I've used Popmotion (react-pose) a lot in the past, and for this article I decided to try it's successor framer-motion. I'm really enjoying it so far, especially because it's so easy to get shit done. Anyway, let me show you what I ended up with.

Instead of a 'normal' React div element, we're going to use framer-motion's div element. So basically it's just another div with a bunch of props that accept animation related stuff. For more background I recommend to check out their docs (which are really good btw)

import { motion } from "framer-motion";

function Notification({ title, description, type, onClose, onMore }) {
  return (
    <motion.div
      initial={{ opacity: 0, scale: 0.8, x: 300 }} // animate from
      animate={{ opacity: 1, scale: 1, x: 0 }} // animate to
      exit={{ opacity: 0, scale: 0.8, x: 300 }} // animate exit
      // describe transition behavior
      transition={{
        type: "spring",
        stiffness: 500,
        damping: 40,
      }}
      // auto animates the element when it's position changes
      positionTransition
    >
      {/* Skipping code for brevity */}
    </motion.div>
  );
}

In order to keep track of which <Notifications /> should animate in or out, we need to wrap these <Notifications /> inside a special <AnimatePresence /> component. So here's how that looks:

import { AnimatePresence } from "framer-motion";
function NotificationProvider({ children }) {
  const notificationRoot = useDomElement();

  const { notify, notifications } = useNotifications();

  return (
    <>
      <NotifyContext.Provider value={notify}>{children}</NotifyContext.Provider>

      {notificationRoot &&
        React.createPortal(
          <NotificationsContainer>
            <AnimatePresence>              {notifications.map(notification => (
                <Notification key={notification.id} {...notification} />
              ))}
            </AnimatePresence>          </NotificationsContainer>,
          notificationRoot
        )}
    </>
  );
}

That was easy, right?

The end 👋🏻

Be sure to check out the complete code on CodeSandbox.

Thanks for reading! I hope you liked it, and stay tuned for more articles! And please, don't be afraid to leave your feedback below.