
TL;DR
If you just want to see the code, it ishere. Andthis is the PR with the latest changes that are discussed in the post.
Disclaimer
For this example we will be usingReact Native Paper
. It greatly helps with design and saves development time. It might take you some extra steps tointegrate, 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
ThenotificationCenter
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.
exportconstnotificationCenterMachine=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:{}},},},});
The machine listens for theNOTIFY
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 thenotificationCenter
reference -NotificationSnackbar
andNotificationModal
. They expect therefNotificationCenter
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.
exportfunctionNavigation(){const{send,state}=useApp();return(<NavigationContaineronReady={()=>{send({type:"START_APP"});}}ref={navigationRef}><RootNavigator/>{state.context.refNotificationCenter&&(<><NotificationSnackbaractor={state.context.refNotificationCenter}/><NotificationModalactor={state.context.refNotificationCenter}/></>)}</NavigationContainer>);}
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 thereceptionist 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})=>{returnsystem.get("notificationCenter");},(_,params:{notification:Notification})=>{return{type:"NOTIFY",notification:params.notification,};},)
Unfortunately, similarly tosendParent
, we loose our type-safety. As a workaround, I'm using a simple util to guarantee that the notification is in the right format:
exportfunctiongetNotificationCenterEvent({},params:{notification:Notification},){return{type:"NOTIFY",notification:params.notification,};}
sendToNotificationCenter:sendTo(({system})=>{returnsystem.get("notificationCenter");},getNotificationCenterEvent)
After having the action registered with thesetup
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}.`,},};},}
You can check the end results by opening theList
screen and play around with the new functionalities.
Conclusion
Leaving all the advantages aside, I was expecting better type-safety with thesendTo
action in combination with thesystem.get()
method. Currently, the situation is similar to what we can achieve withsendParent
. However, the flexibility in communication provided by the receptionist pattern enhances the developer experience.
Secondly, this is the first time I've experimented withenqueueActions()
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)
For further actions, you may consider blocking this person and/orreporting abuse