DEV Community

Cover image for React Native notification center with xState v5
Georgi Todorov
Georgi Todorov

Posted on

React Native notification center with xState v5

TL;DR

If you just want to see the code, it is here. And this is the PR with the latest changes that are discussed in the post.

Disclaimer

For this example we will be using React Native Paper. It greatly helps with design and saves development time. It might take you some extra steps to integrate, but it is easy and intuitive to use.

Background

After setting the basics of the app architecture, we can now continue with the introduction of other important functionalities. In this post we will have a look at how we handle any sort of in-app notifications/messages that we want to display to the user.

While working on our application, we observed that the ways of providng the user with proper visual feedback grow. To manage all our snackbars/modals/dialogs/banners, we decided to move their orchestration in a separate machine.

Notification Center

The notificationCenter machine is spawned on project initilization. Its reference is kept at root level so that it is be available for all spawned children to communicate with.
For this example we are only handling two types of notification feedbacks but it is easily extendable.



export const notificationCenterMachine = setup({
  types: {
    context: {} as {
      snackbar: {
        type: Extract<NotificationType, "snackbar">;
        message: string;
        severity: NotificationSeverity;
      };
      modal: {
        type: Extract<NotificationType, "modal">;
        title: string;
        message: string;
      };
    },
    events: {} as
      | {
          type: "NOTIFY";
          notification: Notification;
        }
      | { type: "OPEN_SNACKBAR" }
      | { type: "OPEN_MODAL" }
      | { type: "CLOSE" },
  },
}).createMachine({
  context: {
    snackbar: {
      type: "snackbar",
      message: "",
      severity: "error",
    },
    modal: { type: "modal", message: "", title: "" },
  },
  id: "notification",
  initial: "idle",
  on: {
    NOTIFY: {
      actions: enqueueActions(({ event, enqueue }) => {
        if (event.notification.type === "snackbar") {
          enqueue.assign({ snackbar: event.notification });
          enqueue.raise({ type: "OPEN_SNACKBAR" });
        }

        if (event.notification.type === "modal") {
          enqueue.assign({ modal: event.notification });
          enqueue.raise({ type: "OPEN_MODAL" });
        }
      }),
    },
    OPEN_SNACKBAR: {
      target: ".snackbar.open",
    },
    OPEN_MODAL: {
      target: ".modal.open",
    },
  },
  type: "parallel",
  states: {
    idle: {},
    snackbar: {
      initial: "closed",
      states: { open: { on: { CLOSE: { target: "closed" } } }, closed: {} },
    },
    modal: {
      initial: "closed",
      states: { open: { on: { CLOSE: { target: "closed" } } }, closed: {} },
    },
  },
});


Enter fullscreen mode Exit fullscreen mode

The machine listens for the NOTIFY event, which based on the type of notification it receives, keeps the latest notification in the context. The notification is then read and displayed/closed by the corresponding component.

Feedback

There are two components that are subscribed to the notificationCenter reference - NotificationSnackbar and NotificationModal. They expect the refNotificationCenter as prop and based on the state decide whether to show the notification. The components are rendered outside of the so that they are available to all application screens.



export function Navigation() {
  const { send, state } = useApp();

  return (
    <NavigationContainer
      onReady={() => {
        send({ type: "START_APP" });
      }}
      ref={navigationRef}
    >
      <RootNavigator />
      {state.context.refNotificationCenter && (
        <>
          <NotificationSnackbar actor={state.context.refNotificationCenter} />
          <NotificationModal actor={state.context.refNotificationCenter} />
        </>
      )}
    </NavigationContainer>
  );
}


Enter fullscreen mode Exit fullscreen mode

Communication

The issue with this approach in xState v4 was that we couldn't predict how deep our actor tree would grow. Sending events between siblings and grandparents in was not straightforward. In case we needed to send an event through several levels of hierarchy, each actor should act as a middleman and resend the event to its parent until the final goal is reached.

The new actor system makes it a lot easier with the receptionist pattern. XState creates implicitly a system, which now gives us a chance to reach out the notification center by sending single event from any child machine.



sendToNotificationCenter: sendTo(
  ({ system }) => {
    return system.get("notificationCenter");
  },
  (_, params: { notification: Notification }) => {
    return {
      type: "NOTIFY",
      notification: params.notification,
    };
  },
)


Enter fullscreen mode Exit fullscreen mode

Unfortunately, similarly to sendParent, we loose our type-safety. As a workaround, I'm using a simple util to guarantee that the notification is in the right format:



export function getNotificationCenterEvent(
  {},
  params: { notification: Notification },
) {
  return {
    type: "NOTIFY",
    notification: params.notification,
  };
}


Enter fullscreen mode Exit fullscreen mode


sendToNotificationCenter: sendTo(({ system }) => {
  return system.get("notificationCenter");
}, getNotificationCenterEvent)


Enter fullscreen mode Exit fullscreen mode

After having the action registered with the setup method, we can simply call it with:



{
  type: "sendToNotificationCenter",
  params: ({ event }) => {
    return {
      notification: {
        type: "snackbar",
        severity: "success",
        message: `You've added an item with id ${event.output.item.id}.`,
      },
    };
  },
}


Enter fullscreen mode Exit fullscreen mode

You can check the end results by opening the List screen and play around with the new functionalities.

screenshot 1

screenshot 2

screenshot 3

Conclusion

Leaving all the advantages aside, I was expecting better type-safety with the sendTo action in combination with the system.get() method. Currently, the situation is similar to what we can achieve with sendParent. However, the flexibility in communication provided by the receptionist pattern enhances the developer experience.
Secondly, this is the first time I've experimented with enqueueActions() and I'm beginning to see its potential. It is different from what I've been used to, but it can greatly simplify state machines.
Next, I plan to implement a registration wizard/funnel.

Top comments (0)