Greetings, developers!
I'd like to show you my take on how to build a simple global notifications system with superstate and React.
We have one extra, implicit goal: building something with satisfying ergonomics and developer experience.
With no further ado, shall we?
Prerequisites
I'm going to create a brand new create-react-app
application with TypeScript:
yarn create react-app superstate-notifications --template typescript
Note that I am using yarn
, but you can mimic my commands using npm
as well.
Once it's done, let's move our working directory to the superstate-notifications
application we just created:
cd superstate-notifications
And then let's install superstate:
yarn add @superstate/core
Cool. Now we have a project that's good to go.
What is superstate?
In short, superstate is a micro state management library for JavaScript applications. Despite the nuances, you can think of it as an alternative solution for Redux or Zustand.
It was designed with developer wellness in mind, and comes bundled with a powerful and handy drafts system to make our lives easier and less repetitive.
Getting started
Now that you have a working project to get your hands dirty, let's create a notifications.tsx
file within src/
and bootstrap the state of our notifications:
import { superstate } from '@superstate/core'
const notifications = superstate([])
Note the []
within superstate()
. That's the initial value of your state. It's as if you'd have typed:
const notifications = []
Except that you wrapped the empty array inside a superstate, and that gives us powers.
Creating & destroying notifications
The next step is creating the two most important functions of the notifications feature: notify
and destroy
. Respectively, one is meant to issue new notifications and the other is to destroy them.
This is what I came up with:
function notify(message: string) {
const id = Math.random().toString()
notifications.set((prev) => [...prev, { id, message }])
}
function destroy(id: string) {
notifications.set((prev) => prev.filter((p) => p.id !== id))
}
The notify
function
It expects to receive a message
(of type string
) argument. This message is what the user is going to see once the notification pops up.
Also, this function declares an id
variable and assigns Math.random().toString()
to it. This is just because we want our system to support multiple notifications at once, and we must have a way to differentiate one notification from another—id
is the way.
Furthermore, the notify
function calls .set()
from our notifications
object. If you scroll up a little, you're going to notice this notifications
object is our superstate()
variable, thus .set()
is a function returned from it.
It may look complicated at first, but all we're doing is passing to .set()
a function that returns what the list of notifications should look like once we emit this new one.
prev
is the previous value of notifications
. Initially, the value of notifications
is []
(an empty array), but as we start emitting notifications, this array will eventually grow—so prev
ensures that we're adding new notifications instead of replacing them.
Look at what we are doing again:
notifications.set((prev) => [...prev, { id, message }])
It means the next value of notifications
is the former notifications plus the new one, which is represented by an object with the id
and message
properties.
The destroy
function
Here we are telling that the next value of notifications
is all notifications but the one that matches the specified id
passed through the argument of the destroy
function:
notifications.set((prev) => prev.filter((p) => p.id !== id))
Rendering notifications
Now in this same notifications.tsx
file, let's create a Notifications Renderer. Its job is critical: displaying the notifications to the user.
Here's the bootstrap of it:
export function NotificationsRenderer() {
useSuperState(notifications)
return null
}
Wait, what? Where is this useSuperState()
function coming from?
Yeah, I did not mention it so far. Intentionally. In order to integrate superstate with React, you have to install an extra dependency:
yarn add @superstate/react
And import it in your notifications.tsx
file:
import { useSuperState } from '@superstate/react'
The useSuperState
hook re-renders our component (NotificationsRenderer) every time the state passed to it changes. In our context, this "state passed to it" refers to notifications
.
This is what I came up with to make the renderer fully functional:
export function NotificationsRenderer() {
useSuperState(notifications)
if (!notifications.now().length) {
return null
}
return (
<div>
{notifications.now().map((n) => {
return (
<div key={n.id}>
<p>{n.message}</p>
<button onClick={() => destroy(n.id)}>
Destroy
</button>
</div>
)
})}
</div>
)
}
Let's break it down:
if (!notifications.now().length) {
return null
}
The if
above guarantees that nothing will be rendered when no notifications exist. Note the now()
method—it returns the current value of your notifications
array. The condition states that if there are no items in the notifications
list, then we'd like to render null
.
{notifications.now().map((n) => {
The line above will iterate over each item in the notifications
array and return something. In our context, for each notification, something will be rendered. Note that now()
is present again.
return (
<div key={n.id}>
<p>{n.message}</p>
<button onClick={() => destroy(n.id)}>
Destroy
</button>
</div>
)
The lines above refer to the actual notification item that will be rendered in the browser and displayed to the user.
As of the last piece of the rendering puzzle, let's open ./src/App.tsx
and clear the returned component to look something like this:
export default function App() {
return ()
}
With the house clean, we can now render our renderer:
import { NotificationsRenderer } from './notifications'
export default function App() {
return (
<div>
<NotificationsRenderer />
<button>Give me a notification!</button>
</div>
)
}
Emitting notifications
You may have noticed we created a Give me a notification!
button at the above post section but have done nothing with it. Well, yet.
Let's make it give us a notification whenever it is clicked:
<button onClick={() => notify('Hello world!')}>
Give me a notification!
</button>
The notify
function won't work right away. We first have to export it. Go back to notifications.tsx
and export both notify
and destroy
functions by prepending the export
keyword in front of the function
keyword:
export function notify(message: string) {
const id = Math.random().toString()
notifications.set((prev) => [...prev, { id, message }])
}
export function destroy(id: string) {
notifications.set((prev) => prev.filter((p) => p.id !== id))
}
Now, at App.tsx
, you'll be able to import them:
import {
notify,
destroy,
NotificationsRenderer,
} from './notifications'
And boom! Save all your files and go to your browser to play with your fresh notifications system. :)
Wrapping up
Your final notifications.tsx
should look like this:
import { superstate } from '@superstate/core'
import { useSuperState } from '@superstate/react'
const notifications = superstate([])
export function notify(message: string) {
const id = Math.random().toString()
notifications.set((prev) => [...prev, { id, message }])
}
export function destroy(id: string) {
notifications.set((prev) => prev.filter((p) => p.id !== id))
}
export function NotificationsRenderer() {
useSuperState(notifications)
if (!notifications.now().length) {
return null
}
return (
<div>
{notifications.now().map((n) => {
return (
<div key={n.id}>
<p>{n.message}</p>
<button onClick={() => destroy(n.id)}>
Destroy
</button>
</div>
)
})}
</div>
)
}
And your App.tsx
:
import {
notify,
destroy,
NotificationsRenderer,
} from './notifications'
export default function App() {
return (
<div>
<NotificationsRenderer />
<button onClick={() => notify('Hello world!')}>
Give me a notification!
</button>
</div>
)
}
You can see a slightly fancier example on StackBlitz:
Final thoughts
This is a pretty basic notifications system, but quite powerful and intuitive. Now, to dispatch notifications in your app, all you have to do is calling the notify()
function you created yourself from anywhere in your app, including non-React code, and have fun because things will just work.
Now you go have some fun and don't hesitate to reach out with any questions or feedback! d(^_^)z
Top comments (0)