React Native with xState v5
Tags: reactnative, xstate, typescript, mobile
Published: 2024-05-09 16:08:42

### TL;DR
If you just want to see the code, it is [here](https://github.com/g-todorov/ReactNativeXStateExample).

### 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 our `appMachine`. 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 the `appMachine`, form the tree with its first branches.

In the `appMachine`'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 the `input` 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 the `appMachine` and the `appProvider` is introduced:

```typescript
export const AppContext = createActorContext(appMachine);

export function AppProvider({ children }: React.PropsWithChildren<unknown>) {
  return <AppContext.Provider>{children}</AppContext.Provider>;
}

export function useApp() {
  const actorRef = AppContext.useActorRef();
  const state = useSelector(actorRef, (snapshot) => {
    return snapshot;
  });

  return {
    state,
    send: actorRef.send,
  };
}
```

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

```typescript
export default React.memo(function Home({}: Props) {
  const { state } = useApp();

  return (
    <View>
      <Text>Welcome, {state.context.username}</Text>
    </View>
  );
});
```

It's worth mentioning that there is another [approach](https://stately.ai/blog/2024-02-12-xstate-react-global-state) 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 is `React Navigation`. 

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

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/b0waia8vr5d6bpp0awfp.png)

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

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

First, we set our `navigationRef`. 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, the `navigationRef` has different build-in methods than the standard `navigation` object.

```typescript
export function Navigation() {
  const { send } = useApp();

  return (
    <NavigationContainer
      onReady={() => {
        send({ type: "START_APP" });
      }}
      ref={navigationRef}
    >
      <RootNavigator />
    </NavigationContainer>
  );
}

```

We use the `onReady` listener to `initialize` the `appMachine`. This way we are sure that we already have the `navigationRef` setup, which we will be using in a bit.

Following what we have so far, we set our `appMachine` to be consumed by our `RootNavigator`. 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 as `prop`s 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.

```typescript
export function RootNavigator() {
  const { state } = useApp();

  const isAuthenticating = state.matches("authenticating");
  const isAuthenticated = state.matches("authenticated");

  return (
    <Stack.Navigator screenOptions={{ headerShown: false }}>
      {isAuthenticating && (
        <Stack.Screen name="Authenticating">
          {(props) => {
            return state.context.refAuthenticating ? (
              <AuthenticatingNavigator
                actorRef={state.context.refAuthenticating}
                {...props}
              />
            ) : null;
          }}
        </Stack.Screen>
      )}
      {isAuthenticated && (
        <Stack.Screen name="Authenticated">
          {(props) => {
            return state.context.refAuthenticated ? (
              <AuthenticatedNavigator
                actorRef={state.context.refAuthenticated}
                {...props}
              />
            ) : null;
          }}
        </Stack.Screen>
      )}
    </Stack.Navigator>
  );
}
```

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](https://medium.com/welld-tech/integrate-xstate-with-react-native-and-react-navigation-21ead87391da) is a great example.

Despite that, after some experimentation, I found that this approach might lead to issues. With breaking the `React Navigation` declarative approach, we might loose some built in functionalities like `goBack`. Also, components like `Bottom 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 for `React 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 again `navigationRef` comes to the rescue.

```typescript
import { createNavigationContainerRef } from "@react-navigation/native";

export const navigationRef = createNavigationContainerRef();

export function getCurrentRouteName() {
  if (navigationRef.isReady()) {
    return navigationRef.getCurrentRoute()?.name;
  } else {
    return undefined;
  }
}
```

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

```typescript
export function useNavigator<T>(
  callback: (route: keyof T) => void,
  initialRoute?: keyof T,
) {
  const prevRoute = useRef<string | undefined>();

  useEffect(() => {
    const unsubscribe = navigationRef.addListener("state", (_event) => {
      const screenRoute = getCurrentRouteName();

      if (screenRoute && prevRoute.current !== screenRoute) {
        prevRoute.current = screenRoute;
        callback(screenRoute as keyof T);
      } else if (!screenRoute && initialRoute) {
        callback(initialRoute);
      }
    });

    return unsubscribe;
  }, []);
}
```

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 dispatched `navigate`. This way we synchronise the focused screen and the machine state.

```typescript
export function AuthenticatedNavigator({ actorRef }: Props) {
  const state = useSelector(actorRef, (snapshot) => {
    return snapshot;
  });

  useNavigator<AuthenticatedParamList>((route) => {
    actorRef.send({ type: "NAVIGATE", screen: route });
  });

  return (
    <Stack.Navigator initialRouteName="Home">
      <Stack.Screen name="Home">
        {(props) => {
          return <HomeScreen actorRef={state.context.refHome} {...props} />;
        }}
      </Stack.Screen>
      <Stack.Screen name="List">
        {(props) => {
          return <ListScreen actorRef={state.context.refList} {...props} />;
        }}
      </Stack.Screen>
    </Stack.Navigator>
  );
}
```

```typescript
/* ... */
NAVIGATE: [
  {
    guard: {
      type: "isHomeScreen",
      params: ({ event }) => {
        return {
          screen: event.screen,
        };
      },
    },
    target: ".homeScreen",
  },
  {
    guard: {
      type: "isListScreen",
      params: ({ event }) => {
        return {
          screen: event.screen,
        };
      },
    },
    target: ".listScreen",
  },
],
/* ... */
```

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

```typescript
const state = useSelector(actorRef, (snapshot) => snapshot);
```

### 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 from `createModel` to `typegen`, 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 the `dynamic params` in v5, I still have mixed feeling about ditching `typegen`. 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.