Snackbars in React: An Exercise in Hooks and Context

Roman Tymchyk
The Startup
Published in
5 min readAug 28, 2019

--

Necessity for the Design System

After performing an audit on how we communicate with our users, we landed with a simple system of three classes of communication, and snackbars fit perfectly on the ‘low priority’ end of that spectrum. They are small notifications that show up on the screen when a user performs an action. They are not intrusive at all and appear for just a brief moment.

Snackbars in Material Design (Android)

Ignoring the visuals, I’m going to focus on how we can build a system for managing snackbars on the web.

Dream State

From an engineering standpoint, our snackbar system must easy to use and generally be very lightweight. We want to be able to place one on the screen with just a couple of lines of code. Anything more and it’s overkill.

This means forget render props and higher order components. We don’t want to deal with a messy React tree and wrappers. We don’t want action creators, reducers*, or any of that stuff.

Hooks are a perfect fit here. So lets imagine we have a custom hook that lets us do this and work backwards.

Dream state hook

Globally Accessible

The state of our snackbars (which ones are visible) can be localized to a single centralized component. That same centralized component can be responsible for rendering them. There’s really no need for another component somewhere in the tree to hook into that state (at least not in our use-case).

However, the management of that state (adding, removing) needs to be globally accessible.

Context can let us do just this.

A ‘naive’ provider

Our context provider is now responsible for both displaying the local state of the snackbars (we call them alerts), and for exposing an API for globally managing them.

The other half of the puzzle is we now need a consumer for this provider. It turns out that our custom hook from before is nothing more than a small wrapper around the useContext internal React hook, which consumes our new context.

useSnackBars custom hook definition

This is not perfect yet though. Our provider exposes the state mutator setAlerts directly, but we need something much higher level. We don’t want our components fumbling around with how to add an alert without destroying existing ones, and so on. Think of setAlerts as a private method, and we need to expose a public method that abstracts away the noise.

Higher Level API & Behaviour

A less naive provider

There’s a lot more happening now. Our provider now exposes a new function called addAlert. This function uses the local state mutator useState to properly append a new alert without destroying existing ones. You could do more fancy things here: de-dup alerts, assign unique IDs to alerts so that we can also expose a removeAlert function that makes use of those IDs, and so on. But we’re going to keep it simple.

Most importantly perhaps is that we now make use of useEffect to display each alert for 5 seconds on the screen.

When a new alert is added, the component is re-rendered, and the effect is executed. In that effect, a timer is kicked off to remove the oldest living alert after a delay. This timer is a side-effect of that render.

When an alert is removed as a result of the above timer expiring, this component is re-rendered with the new state of alerts. Just as before, the side-effect will run, creating a new timer. Before doing that though, it will clean up the side-effect from the previous render (by clearing the timer). The timer is essentially restarted (by deleting the old timer and creating a new one).

This render and side-effect process repeats until alerts /activeAlertIds is empty. This is our base case if you want to think of this recursively.

Sprinkling in Optimizations

Our custom hook is now fully operational! We can add alerts easily from anywhere and our provider will display them for us. We could stop here, but there’s room for some optimizations that are pretty easy to achieve.

You’ll notice that value is a new object being created every single time this provider is rendered. That’s not great, because anything consuming this value from the context will potentially also be getting re-rendered.

We can use useMemo to memoize value . There’s no reason this object needs to be re-created every time. We’ll pass to it addAlert as a dependency, i.e. the memoization cache needs to be re-filled if addAlert changes.

Lets memoize the value — but will this work?

But why would addAlert change? Actually, it shouldn’t, and that’s where a small problem lies. addAlert faces the same problem as value did. This function is being re-created every single render, which means our new useMemo hook will continue to return a new object every single time (since the list of dependencies changes every time).

The solution is simple and similar. We want to memoize the function just like we did with the value. The only difference is that now we’re not memoizing a result, we’re memoizing a function that produces some result in the future (a callback). For this, we can make use of useCallback

Now addAlert is stable between renders: it will always maintain referential equality. As a result of, alert will be stable too.

Note that we don’t have any dependencies for this memoization. We just pass [] which means ‘memoize once and always return the same function in the future’. This function doesn’t depend on anything but the local state alerts, but we don’t need to specify it in the list of dependencies because the setAlert mutator provides it to us directly.

Room for Change

There’s a lot of buzz around how you can replace a lot of what Redux does with useContext and useReducer. The combination of these two hooks means we can have a global state and use Redux-like reducers, actions, and dispatchers to mutate that global state. To an extent, it’s possible.

It’s worth thinking about this if you have multiple ways to mutate the state and/or your state is complicated and/or dependant on one another. In our case we don’t have either of those problems. So you could do that. But I won’t 🙃

--

--