1
votes

So I built a login system with React Native, React Navigation and AsyncStorage. If the user clicks on a button he gets logged in and the value of the AsyncStorage Key "@loginuser" gets refreshed. Now I expected that the screen automatically gets refreshed, but I have to close the App and start it again - This is not optimal.

(I also saw React-Native/React navigation redirection after login with redux post, but it is very old)

App.js

import React from 'react';
import { Text, View, StyleSheet, Button, TouchableOpacity, TextInput } from 'react-native';
import DefaultStackNavigation from './components/Navigation/Navigation';


const App = () => {
  return(
    <View>
      <DefaultStackNavigation />
    </View>
  )
}

export default App;

Navigation.js

import React, {useEffect, useState} from 'react';
//React Native
import { Text, View, StyleSheet} from 'react-native';
//Screens
import HomeScreen from '../HomeScreen/HomeScreen'
import AddScreen from "../AddScreen/AddScreen";
import NotificationScreen from "../NotificationScreen/NotificationScreen";
import MenuScreen from "../MenuScreen/MenuScreen";
import SearchScreen from "../SearchScreen/SearchScreen";
import PostJobScreen from "../PostJobScreen/PostJobScreen";
import JobOfferScreen from "../JobOfferScreen/JobOfferScreen";
import ProfileScreen from "../ProfileScreen/ProfileScreen";
import NoneLoggedinScreen from "../NoneLoggedinScreen/NoneLoggedinScreen"
import SignupModal from "../NoneLoggedinScreen/SignupModal"
//React Navigation
import { createStackNavigator } from "@react-navigation/stack";
import { NavigationContainer } from '@react-navigation/native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
//Third Party
import AsyncStorage from '@react-native-async-storage/async-storage';




const Tab = createBottomTabNavigator();

//Tab bar
const HomeTabs = () => {
  return (
    <Tab.Navigator tabBarOptions={{style:{height: 50}}, {showIcon: true}, {showLabel: false}} >
      <Tab.Screen name="Home" component={HomeScreen}/>
      <Tab.Screen name="SearchScreen" component={SearchJobStack}/>
      <Tab.Screen name="AddScreen" component={AddScreen}/>
      <Tab.Screen name="NotificationScreen" component={NotificationScreen} />}}/>
      <Tab.Screen name="MenuScreen" component={MenuScreen}/>}}/>
    </Tab.Navigator>
  );
}

const Stack = createStackNavigator();
const STORAGE_KEY = '@loginStatus'

const DefaultStackNavigation = () => {

  const [loginStatus, setLoginStatus] = useState()
  const readData = async () => {
    try {
      const isLoggedIn = JSON.parse(await AsyncStorage.getItem(STORAGE_KEY))
      console.log(isLoggedIn)
      if (isLoggedIn !== null) {
        setLoginStatus(isLoggedIn)
      }
    } catch (e) {
      alert('Failed to fetch the data from storage')
    }
  }

  readData()

  return loginStatus ? (
    <NavigationContainer>
      <Stack.Navigator screenOptions={{headerShown: false}} independent={false}>
        <Stack.Screen name="HomeTabs" component={HomeTabs} />
        <Stack.Screen name="PostJobScreen" component={PostJobScreen} />
        <Stack.Screen name="ProfileScreen" component={ProfileScreen}/>
      </Stack.Navigator>
    </NavigationContainer>
  ) : (
    <NavigationContainer>
      <Stack.Navigator screenOptions={{headerShown: false}} independent={false}>
        <Stack.Screen name="NoneLoggedinScreen" component={NoneLoggedinScreen} />
        <Stack.Screen name="SignupModal" component={SignupModal} />
      </Stack.Navigator>
    </NavigationContainer>
  );
};


export default DefaultStackNavigation;

NoneLoggedinScreen.js

import React, { useState, useEffect } from 'react';
import { Text, View, Button} from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';

const STORAGE_KEY = '@loginStatus'

const SignupModal = () => {

  const [loginStatus, setLoginStatus] = useState(false)
  const saveData = async (parmLoginStatus) => {
    try {
      await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(parmLoginStatus))
      alert('Data successfully saved -> Logged In')
      console.log(loginStatus)
    } catch (e) {
      alert('Failed to save the data to the storage')
    }
  }

  const onSubmitLogin = () => {
    setLoginStatus(true)
    saveData(true)
  }


  return(
    <View>
      <Text>Login Page. Press the button to log in and stay logged in</Text>
      <Button title="Log in" onPress={() => onSubmitLogin()}/>
    </View>
  );
};

const styles = StyleSheet.create({
  App: {
    flex: 1,
    backgroundColor: "white"
  },
});

export default SignupModal;

Now I was expecting the page to reload and I can use the app. Unfortunately it doesn't. The data is saved and the login status is set to true, but I have to restart the app to use it. The perfidious thing is, if I write the AsyncStorage login logic from the NoneLoggedinScreen.js file into the App.js file, the app works fine -> However, this is not an alternative for me, because the general structure of the app is, I think, built relatively sensibly. Also when the user tries to be redirected manually (by button) with navigation.navigate("HomeTabs") after logging in doesn't work, and I get an error that Home doesn't exist, which is also not understandable, because the navigation has actually been set to Logged in now. Has anyone ever had this problem?

This are my dependencies by the way

"dependencies": {<br>
  "@react-native-async-storage/async-storage": "^1.15.1",<br>
  "@react-native-community/masked-view": "^0.1.10",<br>
  "@react-navigation/bottom-tabs": "^5.11.7",<br>
  "@react-navigation/native": "^5.9.2",<br>
  "@react-navigation/stack": "^5.14.2",<br>
  "react": "16.13.1",<br>
  "react-native": "0.63.4",<br>
  "react-native-gesture-handler": "^1.10.1",<br>
  "react-native-reanimated": "^1.13.2",<br>
  "react-native-safe-area-context": "^3.1.9",<br>
  "react-native-screens": "^2.17.1",<br>
}
2

2 Answers

2
votes

In your authentication strategy, there is a hidden assumption that when you save data using AsyncStorage in NoneLoggedInScreen, the data will immediately be read using the readData function in DefaultStackNavigation. The reason why this does not happen is that readData is only called when DefaultStackNavigation renders/re-renders. This will not happen when one of its children sets some data in local storage.

There are many ways to fix this. Using redux with redux-persist or another state-management and persistence setup, or a purpose-built context-based solution (see eg. this Kent C. Dodds article on Authentication in React Applications).


The second, minor issue that you mention:

navigation.navigate("HomeTabs") after logging in doesn't work, and I get an error that Home doesn't exist

does make sense, and the reason behind it is that you conditionally render two different NavigationContainers. They don't know about each others routes, so you can't navigate between them.

To fix this, you should render one NavigationContainer and conditionally render one of the Stack.Navigators as its child.

0
votes

To manage authentication flow in a smooth way we should make two seperate navigators for the navigation. I find this workflow the best as of now. It works perfectly and is clean. So what I usually do in my Projects that I create two navigators AuthNavigator.js and AppNavigator.js and then use conditional rendering.

So what you can do is create a folder called navigation where your App.js is located. Then inside navigation folder create two files called AppNavigator.js and AuthNavigator.js.

Your AuthNavigator.js should look like this

import React from "react";
import NoneLoggedinScreen from "../NoneLoggedinScreen/NoneLoggedinScreen";
import SignupModal from "../NoneLoggedinScreen/SignupModal";
import { createStackNavigator } from "@react-navigation/stack";

const Stack = createStackNavigator();

const AuthNavigator = () => {
  return (
    <Stack.Navigator screenOptions={{ headerShown: false }} independent={false}>
      <Stack.Screen name="NoneLoggedinScreen" component={NoneLoggedinScreen} />
      <Stack.Screen name="SignupModal" component={SignupModal} />
    </Stack.Navigator>
  );
};

export default AuthNavigator;

Your AppNavigator.js should look like this

import React from "react";
import HomeScreen from "../HomeScreen/HomeScreen";
import AddScreen from "../AddScreen/AddScreen";
import NotificationScreen from "../NotificationScreen/NotificationScreen";
import MenuScreen from "../MenuScreen/MenuScreen";
import PostJobScreen from "../PostJobScreen/PostJobScreen";
import ProfileScreen from "../ProfileScreen/ProfileScreen";
import { createStackNavigator } from "@react-navigation/stack";
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";

const Tab = createBottomTabNavigator();

//Tab bar
const HomeTabs = () => {
  return (
    <Tab.Navigator
      tabBarOptions={
        ({ style: { height: 50 } }, { showIcon: true }, { showLabel: false })
      }
    >
      <Tab.Screen name="Home" component={HomeScreen} />
      <Tab.Screen name="SearchScreen" component={SearchJobStack} />
      <Tab.Screen name="AddScreen" component={AddScreen} />
      <Tab.Screen name="NotificationScreen" component={NotificationScreen} />
      <Tab.Screen name="MenuScreen" component={MenuScreen} />
    </Tab.Navigator>
  );
};

const Stack = createStackNavigator();

const AppNavigator = () => {
  return (
    <Stack.Navigator screenOptions={{ headerShown: false }} independent={false}>
      <Stack.Screen name="HomeTabs" component={HomeTabs} />
      <Stack.Screen name="PostJobScreen" component={PostJobScreen} />
      <Stack.Screen name="ProfileScreen" component={ProfileScreen} />
    </Stack.Navigator>
  );
};

export default AppNavigator;

And your App.js should look like this

import React, { useState, useEffect } from 'react';
import { NavigationContainer } from '@react-navigation/native';

import AuthStorage from './auth/storage';
import AppNavigator from './navigation/AppNavigator';
import AuthNavigator from './navigation/AuthNavigator';

const App = () => {
  const [loginStatus, setLoginStatus] = useState(false);

  // I am using useEffect hook but I would prefer using `AppLoading` to restore token if it exists
  useEffect(() => {
    readData();
  }, []);

  const readData = async () => {
    try {
      const isLoggedIn = JSON.parse(await AuthStorage.getItem());
      console.log(isLoggedIn);
      if (isLoggedIn !== null) {
        setLoginStatus(isLoggedIn);
      }
    } catch (e) {
      alert('Failed to fetch the data from storage');
    }
  };

  return (
    <NavigationContainer>
      {loginStatus ? <AppNavigator /> : <AuthNavigator />}
    </NavigationContainer>
  );
};

export default App;

Create a folder called auth where your App.js is located. Inside that create a file called storage.js. Now inside storage.js paste this code

import AsyncStorage from '@react-native-async-storage/async-storage';

const AuthToken = '@loginStatus';

const storeItem = async (value) => {
  try {
    await AsyncStorage.setItem(AuthToken, JSON.stringify(value));
    return true;
  } catch (e) {
    return false;
  }
};

const getItem = async () => {
  try {
    const jsonValue = await AsyncStorage.getItem(AuthToken);
    return jsonValue != null ? JSON.parse(jsonValue) : null;
  } catch (e) {
    return null;
  }
};

export default { storeItem, getItem };