javascriptreactjsreact-nativereact-navigationmobx-react-lite

React Navigation 5 authentication flow with MST, not "switching"


So, I've just upgraded an ignite-bowser project to their 5.0 template, which includes React Navigation 5, which requires a change from the legacy recommended method of using a SwitchNavigator to swap between 'Auth' and 'App' StackNavigators, to a new declarative auth flow approach that makes the SwitchNavigator redundant.

FYI, Ignite Bowser projects are essentially React Native template apps backed by

React Native
React Navigation
MobX State Tree
TypeScript
Reactotron
And more!

So this all seems simple enough, but I have not been able to get the actual navigators to switch when using a boolean stored in one of the App stores, and set to true in an action called within the authentication method.

According to server logs, and Reactotron feed, authentication works fine. Reloading the app afterward does render the App navigator, but the session isn't actually valid since memory was cleared. All of the subsequent requests fail, but the application doesn't switch to the Auth navigator.

Here are the relevant code snippets:

root-navigator.tsx

const RootStack = () => {
  const { pbidStore } = useStores()

  return (
    <Stack.Navigator
      screenOptions={{
        headerShown: false,
        gestureEnabled: true,
        stackPresentation: "modal",
      }}
    >
      {pbidStore.isAuthenticated ? (
        <Stack.Screen
          name="pbidStack"
          component={PbidNavigator}
          options={{
            headerShown: false,
          }}
        />
      ) : (
        <Stack.Screen
          name="authStack"
          component={AuthNavigator}
          options={{
            headerShown: false,
          }}
        />
      )}

    </Stack.Navigator>
/**
 * PbidStore Model
 */
 export const PbidStoreModel = types.model("PbidStore").props({
   ....

   isAuthenticated: types.optional(types.boolean, false),
 })
 .actions(self => ({
   setStatus(value?:  "idle" | "pending" | "done" | "error") {
     self.status = value
   },
   setAuthToken(token: string) {
     self.environment.pbidApi.setAuthToken(token)
   },
   setAuthenticated(value: boolean) {
     self.isAuthenticated = value
   },
   ...
 }))
 .actions(self => ({
   authenticate: flow(function*(email: string, password: string, remember: boolean) {
     self.setStatus("pending")
     try {
       const result = yield self.environment.pbidApi.authenticate(email, password)
       if (result.kind === "ok") {
         self.setAuthToken(result.token)
         self.setStatus("done")
         self.setAuthenticated(true)
         self.loadUser()
         if(remember)
           yield self.storeCredentials(email, password)
       } else {
         self.setStatus("error")
         self.setAuthenticated(false)
       }
     } catch {
       self.setStatus("error")
       self.setAuthenticated(false)
     }
   }),
...

Solution

  • After I had written out this question, and started picking SO tags, and had to decide upon using mobx-react vs mobx-react-lite or both, I remembered the issues I ran into during the last upgrade I went through, which switched between those two, and the use of inject and observer.

    So I realized that maybe my navigator needed to be observable...

    Importing mobx-react-lite and wrapping RootStack in the following fixed everything for me.

    const RootStack = observer(() => {
    

    Hope this helps save someone else the headache.

    All in all, I'm pleased with what all these recent changes to react-native with the Hooks and FunctionalComponents, and what its done to associated libraries and eventually my code base, but wooof is it tiring having to constantly learn new API's and re-base my projects before they're even complete!