1
votes

I'm using React Navigation on my react native app and following its authentication flow.

app.js will render either authentication screen or home screen based on user auth state called userProfile.

userProfile will be passed from App component to children component via React context called userContext, and some dispatch functions will be passed as well called authContext.

const App: () => React$Node = () => {

  const [state, dispatch] = React.useReducer(
    (prevState, action) => {
      switch (action.type) {
        case SIGN_IN:
          return {
            ...prevState,
            userProfile: action.userProfile,
          };
        case SIGN_OUT:
          return {
            ...prevState,
            userProfile: null,
          };
      }
    },
    {
      isLoading: true,
      userProfile: null,
    }
  )


  React.useEffect(() => {
    isMountedRef.current = true;
    return () => (isMountedRef.current = false);
  }, []);


  const authContext = React.useMemo(
    () => ({
      signIn: async userProfile => {
        try {
          await AsyncStorage.setItem('userProfile', JSON.stringify(userProfile))
          dispatch({ type: SIGN_IN, userProfile })
        } catch (error) {
          console.log(error)
        }
      },
      signOut: async () => {
        try {
          await AsyncStorage.removeItem('userProfile');
          dispatch({ type: SIGN_OUT })
        } catch (error) {
          console.log(error)
        }
      }
    }),
    []
  );

  return (
    <AuthContext.Provider value={{authContext, userContext: state.userProfile}}>
      <NavigationContainer ref={navigationRef}>
        <Stack.Navigator>
          {console.log('app render: ', state.userProfile)}
          {
            state.userProfile == null ?
              (<Stack.Screen name="AuthContainer" component={AuthContainer} options={{ headerShown: false }} />) :
              (<Stack.Screen name="MainContainer" component={MainContainer} options={{ headerShown: false }} />)
          }
        </Stack.Navigator>
        
      </NavigationContainer>
    </AuthContext.Provider>
  );
};

In one of the nested children component ProfileScreen under home screen or under MainContainer in above code, I'm trying to consume the userContext to show user information on screen, and a sign out button is using a dispatch sign out function to update the user auth state to null in the App component.

function ProfileScreen() {
    const { authContext, userContext } = React.useContext(AuthContext)
    console.log('profile render: ', userContext)
    return (
        <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
            <Text>Hello {userContext.username}!</Text>
            <Text>Profile Screen</Text>
            <Button
                title="Sign Out"
                onPress={() => authContext.signOut()}
            />
        </View>
    );
}

export default ProfileScreen;

After dispatch the sign out function, I expect the app to navigate to auth screen AuthContainer to ask user to sign in again since at this time my userProfile state in App component should be null. However, App component is still trying to render the ProfileScreen which throws an error userContext is null.

From my log, after I dispatch sign out function in ProfileScreen, it shows

app render: null     ---> App re-render
profile render: null   ---> profileScreen re-render
profile render: null   ---> profileScreen re-render
auth container   ---> finally start rendering Auth Screen

then immediately throw userContext is null error

Can anyone kindly help me understand why App component tries to render profileScreen when userProfile state is null? And why does the profileScreen re-render twice?

Thans you so much

1

1 Answers

0
votes

It looks like both your conditional Stack.Screens and the ProfileScreen rely on the userProfile state. Because that state is being updated asynchronously (like all things in React), it leads me to believe a race condition is causing your problem.

Ie, you dispatch the action to update your store, but the ProfileScreen userContext.username receives the update before the protected container does state.userProfile == null ? <Screen1 /> : <Screen2 />.

IMO a component which uses data from an outside source must always protect itself against missing values. Especially when you are explicitly nulling that state.

In your case I would just write userContext?.username instead. Or userContext ? <Text>{`Hello ${userContext.username}!`}</Text> : null