Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

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

Posted on • Edited on

     

React Native with xState v5

TL;DR

If you just want to see the code, it ishere.

Disclamer

This not an introduction tutorial for xState nor React Native. It requires some basic knowledge of the libraries and won't be explaining the example code line by line. You can consider it as an opinionated guide on how to structure your application.

Background

In our team we recently released a React Native application that heavily relies on xState. As xState v5 is advancing, we decided that the time for migration from v4 has come. Since, we are already in production, we want to design it properly and use the chance to introduce some improvements along with the upgrade.
As I'm in charge of the migration, I'm planning to use this post/series as both playground and documentation. More features will be added eventually, until most of our xState cases are mimicked with version 5. I'm not going to make a comparison between v4 and v5, but instead I will set a brand new project with the latest version. Hopefully, sharing some of our key principles when working together with React Native and xState, will bring constructive feedback.

State orchestration

Working with xState and React Native, I usually build my app by integrating my view library in xState, instead of the other way round. This might feel as an overkill but I like the control it gives me over the application and how business logic organically finds its proper place in the machines, leaving the screens/components stateless.

I like to form the app around a treelike machine structure by using the actor model that xState adapts. The root is ourappMachine. Its states form the flow of the application. Each state has a dedicated spawned actor that is assigned in the machine context. These actors, along with theappMachine, form the tree with its first branches.

In theappMachine's context, except the actor references, we can also keep data that is considered global for the application (profile data, languages settings, etc.).
Once having it there, we can pass it to the spawned actor children via theinput prop.

On the other hand we might need the global data just for visual purposes as displaying profile settings, which makes passing it around the spawned children useless. That's why, along with theappMachine and theappProvider is introduced:

exportconstAppContext=createActorContext(appMachine);exportfunctionAppProvider({children}:React.PropsWithChildren<unknown>){return<AppContext.Provider>{children}</AppContext.Provider>;}exportfunctionuseApp(){constactorRef=AppContext.useActorRef();conststate=useSelector(actorRef,(snapshot)=>{returnsnapshot;});return{state,send:actorRef.send,};}
Enter fullscreen modeExit fullscreen mode

And then we can consume the data from a react view:

exportdefaultReact.memo(functionHome({}:Props){const{state}=useApp();return(<View><Text>Welcome,{state.context.username}</Text></View>);});
Enter fullscreen modeExit fullscreen mode

It's worth mentioning that there is anotherapproach for dealing with global state.

Navigation

In order to give meaning to our application flow, we need to visualize what our tree represents. We can achieve this by introducing a navigation instrument and my weapon of choice isReact Navigation.

I found this part to be the hardest to implement but the solution has proven with time.

Image description

Following the diagram, I'll try to explain how the actor system is working with the navigation.

I've already mentioned thatappMachine carries a special role when integrating with React Native and the same is valid for React Navigation.

First, we set ournavigationRef. Despite it not the recommended way of navigating through screens, it might be convenient. It is really useful when you want to navigate from your machine after asynchronous actor has finished. Also, thenavigationRef has different build-in methods than the standardnavigation object.

exportfunctionNavigation(){const{send}=useApp();return(<NavigationContaineronReady={()=>{send({type:"START_APP"});}}ref={navigationRef}><RootNavigator/></NavigationContainer>);}
Enter fullscreen modeExit fullscreen mode

We use theonReady listener toinitialize theappMachine. This way we are sure that we already have thenavigationRef setup, which we will be using in a bit.

Following what we have so far, we set ourappMachine to be consumed by ourRootNavigator. As mentioned earlier, each root state has an actor reference that is spawned when entering the state and stopped when exiting the state. These actors become our navigator machines. They will respectively be passed asprops to their own<Stack.Navigator/>. The important bit from here is that only one<Stack.Navigator/> can exist at a time, depending on the root machine state, which works great for protecting routes.

exportfunctionRootNavigator(){const{state}=useApp();constisAuthenticating=state.matches("authenticating");constisAuthenticated=state.matches("authenticated");return(<Stack.NavigatorscreenOptions={{headerShown:false}}>{isAuthenticating&&(<Stack.Screenname="Authenticating">{(props)=>{returnstate.context.refAuthenticating?(<AuthenticatingNavigatoractorRef={state.context.refAuthenticating}{...props}/>):null;}}</Stack.Screen>)}{isAuthenticated&&(<Stack.Screenname="Authenticated">{(props)=>{returnstate.context.refAuthenticated?(<AuthenticatedNavigatoractorRef={state.context.refAuthenticated}{...props}/>):null;}}</Stack.Screen>)}</Stack.Navigator>);}
Enter fullscreen modeExit fullscreen mode

When integrating xState with React Navigation, I see two possible approaches:

  1. xState driven navigation
  2. React Navigation driven navigation

Most of the examples/tutorials I've encountered, go with the first approach. Listening for changes in the machine state and then navigating to the corresponding page. Possibly, is the expected path when you build your app around xState.Here is a great example.

Despite that, after some experimentation, I found that this approach might lead to issues. With breaking theReact Navigation declarative approach, we might loose some built in functionalities likegoBack. Also, components likeBottom Tabs Navigator tend to encapsulate the event handling, which means that extra development would be required in order to work properly with the paradigm. This led me thinking that this might not be the right direction.

On the other hand, listening for changes in the navigation state and updating your machine state accordingly, leaves all the heavy lifting forReact Navigation.

The implementation is not straightforward but can be gathered in several steps.

First, we need a method that gives us the current route, and againnavigationRef comes to the rescue.

import{createNavigationContainerRef}from"@react-navigation/native";exportconstnavigationRef=createNavigationContainerRef();exportfunctiongetCurrentRouteName(){if(navigationRef.isReady()){returnnavigationRef.getCurrentRoute()?.name;}else{returnundefined;}}
Enter fullscreen modeExit fullscreen mode

Now we can prepare our hook that will be listening for the route changes.

exportfunctionuseNavigator<T>(callback:(route:keyofT)=>void,initialRoute?:keyofT,){constprevRoute=useRef<string|undefined>();useEffect(()=>{constunsubscribe=navigationRef.addListener("state",(_event)=>{constscreenRoute=getCurrentRouteName();if(screenRoute&&prevRoute.current!==screenRoute){prevRoute.current=screenRoute;callback(screenRouteaskeyofT);}elseif(!screenRoute&&initialRoute){callback(initialRoute);}});returnunsubscribe;},[]);}
Enter fullscreen modeExit fullscreen mode

The hook is supposed to be used only in<Stack.Navigator/>s. Each navigator's machine has a root event that handles the machine state change after the dispatchednavigate. This way we synchronise the focused screen and the machine state.

exportfunctionAuthenticatedNavigator({actorRef}:Props){conststate=useSelector(actorRef,(snapshot)=>{returnsnapshot;});useNavigator<AuthenticatedParamList>((route)=>{actorRef.send({type:"NAVIGATE",screen:route});});return(<Stack.NavigatorinitialRouteName="Home"><Stack.Screenname="Home">{(props)=>{return<HomeScreenactorRef={state.context.refHome}{...props}/>;}}</Stack.Screen><Stack.Screenname="List">{(props)=>{return<ListScreenactorRef={state.context.refList}{...props}/>;}}</Stack.Screen></Stack.Navigator>);}
Enter fullscreen modeExit fullscreen mode
/* ... */NAVIGATE:[{guard:{type:"isHomeScreen",params:({event})=>{return{screen:event.screen,};},},target:".homeScreen",},{guard:{type:"isListScreen",params:({event})=>{return{screen:event.screen,};},},target:".listScreen",},],/* ... */
Enter fullscreen modeExit fullscreen mode

In a similar manner as the navigator actors, each screen might have (or not) a dedicated actor reference which will be passed asprop to the screen component and used withuseSelector from there.

conststate=useSelector(actorRef,(snapshot)=>snapshot);
Enter fullscreen modeExit fullscreen mode

Conclusion

Instead of praising how good xState is, I would like to leave couple of words what my first impressions are after I've started writing production code with v5.
As almost in the beginning of the project, I had a transition fromcreateModel totypegen, I ensure you that I wasn't really keen on rewriting even larger codebase with new typing approach.
To be honest, though I feel the power of thedynamic params in v5, I still have mixed feeling about ditchingtypegen. Despite it introduces CI complexity, I feel like it was more of a helper than a burden.
On the other hand, typing actors/actions outside of the machine is something that I'm exited about.

Top comments(2)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss
CollapseExpand
 
arga_runchise profile image
Arga - Runchise
  • Work
    Software Engineer @ Runchise
  • Joined

Hi, with the approach in the article, is it possible to prevent route navigation based on the guards in the machine?

CollapseExpand
 
gtodorov profile image
Georgi Todorov
Software developer.
  • Location
    Sofia, Bulgaria
  • Joined

@arga_runchise If I understand you correctly, you can always make the guard stricter and add more authorization rules. For example:

isHomeScreen(_,params:{screen:keyofAuthenticatedParamList}){returnparams.screen==="Home";},
Enter fullscreen modeExit fullscreen mode

to

isHomeScreen({context},params:{screen:keyofAuthenticatedParamList}){returnparams.screen==="Home"&&context.isUserAllowed;},
Enter fullscreen modeExit fullscreen mode

Still, I personally prefer to protect routes at navigator level (in theuseApp.tsx).
Let me know if this makes sense to you.

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

Software developer.
  • Location
    Sofia, Bulgaria
  • Joined

More fromGeorgi Todorov

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp