Notification

Notifications, toasts, and snackbars – however you name them – are essential UI elements suitable to be implemented with a state machine. They use a timer, and this single reason is enough to use XState for me.

This notification example allows only a single notification to be opened simultaneously. Stacking notifications will be showcased on another machine and involve spawned actors.

The Notification Center example is based on this. It adds the feature of stacked notifications and manages it with actors. You can check it out for a more realistic sample.

Example

A notification can be triggered only in the Closed state, that is, when the notification is not opened yet. Conversely, it can only be closed in the Open state.

When entering the Open state, it will first go to the Checking if timer is required state. This state, often called a determining state, checks if a timeout has been defined for the notification. If a timeout was defined, it goes to Waiting for timeout. Otherwise, it goes to Waiting for manual action. The machine uses an eventless transition with the always keyword. This state is exited immediately after entering.

The close event is listened to in both Waiting for timeout and Waiting for manual action states. When the close event is received, the machine goes to the Done state, whose type is final. The Open state defines an onDone transition, taken when it reaches one of its child final states, and makes the machine go back to Closed state.

The benefit of the Done state is explicitly saying that it’s the end of the Closed state. It also acts as a layer of abstraction: the child states of Open don’t have to know about states outside their parent.

Final states are a powerful feature of state charts, especially with parallel states.

Trigger a notification

Notification method

Code

View in GitHub
machine.ts
import { assertEvent, assign, setup } from "xstate";
export const notificationMachine = setup({
types: {
context: {} as {
timeout: number | undefined;
title: string | undefined;
description: string | undefined;
},
events: {} as
| {
type: "trigger";
timeout?: number;
title: string;
description: string;
}
| { type: "close" },
},
delays: {
"Notification timeout": ({ context }) => {
if (context.timeout === undefined) {
throw new Error("Expect timeout to be defined.");
}
return context.timeout;
},
},
actions: {
"Assign notification configuration into context": assign(({ event }) => {
assertEvent(event, "trigger");
return {
title: event.title,
description: event.description,
timeout: event.timeout,
};
}),
},
guards: {
"Is timer defined": ({ context }) => typeof context.timeout === 'number',
}
}).createMachine({
id: "Notification",
context: {
timeout: undefined,
title: undefined,
description: undefined,
},
initial: "Closed",
states: {
Closed: {
on: {
trigger: {
target: "Open",
actions: "Assign notification configuration into context",
},
},
},
Open: {
initial: "Checking if timer is required",
states: {
"Checking if timer is required": {
always: [
{
guard: "Is timer defined",
target: "Waiting for timeout",
},
{
target: "Waiting for manual action",
},
],
},
"Waiting for timeout": {
after: {
"Notification timeout": {
target: "Done",
},
},
on: {
close: {
target: "Done",
},
},
},
"Waiting for manual action": {
on: {
close: {
target: "Done",
},
},
},
Done: {
type: "final",
},
},
onDone: {
target: "Closed",
},
},
},
});

Get news from XState by Example

Sign up for the newsletter to be notified when more machines or an interactive tutorial are released. I respect your privacy and will only send emails once in a while.