Not Your Usual React.js Notification Bar 🫠

Not Your Usual React.js Notification Bar 🫠

I ran into an issue while working on a react codebase. I knew what I had to do to fix it but it just didn't feel right. So, what did I do?

Before I tell you that, let me explain the problem.

Error handling after making API calls can be a big pain with react js. I typically create a custom error component and I conditionally render it based on the state of the API call. You might be asking why not use Redux or Context API? Well, in cases where I have to nail the UX and have the user retry the action on error, I really would want to keep the action in scope. Pushing the function to redux is not an option either as it doesn't serialize functions. Context API on the other hand makes your entire application codependent (IMO). So, I had to think of something else.

Hello Custom Events

Pretty sure y'all didn't see this coming. After spending some hours pondering on it, I started relating react.js with vanilla js (Plain Javascript), and then a tool I built about a year ago (2021) came to mind. I recalled using a custom event to pass information between two js files, simply because I wanted to separate both logic.

At this point, I knew this would be the core of my solution. Designing the behaviour of the component was not so difficult thanks to a package react-toastify. I was always intrigued by its behaviour and decided to imitate it. Not the source code, just the behaviour.

You basically place the Container component at the root of your application, and from anywhere within the application, you can trigger a notification by a function call. Which is exactly what my solution looked like. Here's a little illustration.

Twitter post - 1.png

Getting Started

You can fork the source code off of stackblitz for the full implementation. I'm going to skip project initialization and styling and just focus on showing you what the logic looks like.

First we create a custom event type that allows us to pass a message field to the emitted event.

export type NotificationEvent = Event & {
  message: string;
};

Then we create the Notify function which looks something like this

const Notify = (message: string) => {
  const notificationEvent = new Event('notification') as NotificationEvent;
  notificationEvent.message = message;
  dispatchEvent(notificationEvent);
};

export default Notify;

The Notify function takes a message parameter which is essentially what you need to broadcast across your application. It then creates a new Event instance and adds the message to the event's message field. Meaning when we finally dispatch the event, we will be sending the message along with it.

Finally we need to listen for the event Notify is firing and that's as simple as saying

window.addEventListener('notification', pushNotification as EventListener);

Above, we listen for the notification event which is what Notify is firing.

Some things to note at this point

  • I created the event instance within the Notify function. Creating it outside the function would reference the same event and cause our component not to re-render.

  • pushNotification is a custom function that sets the state of our NotificationBar Component.

Creating the Notification bar component

We start by creating a bunch of component states.

  const hasComponentMounted = useRef(false);
  const timeOut = useRef(null);
  const [hasTimedOut, setHasTimedOut] = useState(true);
  const [notification, setNotification] = useState({} as NotificationEvent);
  • hasComponentMounted is used to prevent the component from rendering twice. This behaviour is caused by React.StrictMode during development. You can read more on that here
  • timeOut is used to store the setTimeout session in progress. This is because, we want our notification bar to hide the message after some time has passed, while being able to override the current notification in the event another notification is dispatched before the previous' time has elapsed.
  • hasTimedOut is used to conditionally render the message bar
  • notification is where we store the notification event being dispatched across our app.

Next, we create two helper functions that set and clear the notification bar

function clearNotifications() {
    clearTimeout(timeOut.current);
    setHasTimedOut(true);
}

function pushNotification (e: NotificationEvent) {
    clearNotifications();
    setNotification(e);
};

Then we register our event listener

  useEffect(() => {
    if (hasComponentMounted.current) return;
    window.addEventListener('notification', pushNotification as EventListener);
    hasComponentMounted.current = true;
  }, []);

Now remember what I said about React.StrictMode, this is how I prevented it from breaking. Another thing you could do is simply remove it from your application. You'll typically find it in your main.tsx file.

NOTE: We need to ensure the event listener is only registered once, across the entire application otherwise, we risk having multiple listeners triggering per event fired/dispatched.

So far, we've registered our event listener and are successfully updating the notification state. Now we just need to respond to that change and toggle the hasTimedOut state to conditionally render the notification fired. To do this, we create another useEffect and pass the notification state as a member of the dependency array like so:

  useEffect(() => {
    if (notification.message) {
      setHasTimedOut(false);
      timeOut.current = setTimeout(() => {
        clearNotifications();
      }, 3000);
    }

    return () => {
      clearNotifications();
    };
  }, [notification]);

And finally:

  if (hasTimedOut) return null;

  return (
    <div className="notification-bar">
      <div className={'message'}>{notification.message}</div>
    </div>
  );

Below is an example where I dispatch a notification on a button click

function App() {
  const [count, setCount] = useState(1);

  function handleClick() {
    setCount((c) => c + 1);
    Notify('Notification is active ' + count);
  }

  return (
    <div className="App">
      <button onClick={handleClick}>Notify!</button>
    </div>
  );
}

To use the NotificationBar component, we import it in our root file, like so:

ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
  <React.StrictMode>
    <App />
    <NotificationBar />
  </React.StrictMode>
);

You can find the full demo here

Final Notes

So, that is how you create and use an unusual notification bar in your react applications. I hope that wasn't too overwhelming, this is a lightweight version of the original implementation. Definitely can be improved upon.

Kindly leave a comment if you spotted anything wrong with this approach, or if this approach is bad in general.

Stay Jiggy and see you in the next one!

NYUG 🫠

Â