Authentication flow is an essential part of any modern mobile application. In this article, we will see how to store and manage the authentication state in React Native.
Storing auth data
Let's consider a basic scenario where we use a phone number and authentication token for verifying API requests. Here we need to persist both the phone number and authentication token. For storing these data we can use AsyncStorage, which is a key-value data store commonly used to persist data locally in React Native. Even though we are storing two different values, we will always access them together and hence we can avoid multiple AsyncStorage calls by storing them as a single key-value pair.
1const auth = { 2 phone: PHONE_NUMBER, // User's phone number 3 token: AUTH_TOKEN, // Authentication token 4}; 5 6AsyncStorage.setItem("auth", JSON.stringify(auth)); 7// AsyncStorage supports only string values - so we have to serialize our object.
After storing the data we can use the AsyncStorage.getItem() API to retrieve it.
Things to keep in mind while using AsyncStorage
It should be clear from the name that AsyncStorage calls are asynchronous so consider the following while using it.
-
Avoid unnecessary AsyncStorage calls - Async calls will take time and may affect user experience.
-
Reuse data once fetched - Use higher-level state or central state management like Context, Redux etc to manage data used in multiple places.
-
Asynchronous calls might fail and we should handle exceptions properly.
-
In Android, AsyncStorage has a 6MB hard limit. You can change it by setting AsyncStorage_db_size_in_MB value in gradle.properties file.
-
AsyncStorage uses SQLite so beware of SQLite limits.
Manage auth state globally with Context API
In many apps we would use the current authentication state to display appropriate content based on auth state, user type etc., so it's always good to put it in a centrally accessible place. We can use a central state management library like Redux or Context API. In this example we will use the built-in Context API.
Let's create an AuthContext and store all auth data in the Provider state by fetching it from AsyncStorage. We will also an API to update the data in state & AsyncStorage.
AuthContext
1// Create a context 2const AuthContext = createContext({}); 3 4const AuthProvider = ({ children }) => { 5 const [auth, setAuthState] = useState(initialState); 6 7 // Get current auth state from AsyncStorage 8 const getAuthState = async () => { 9 try { 10 const authDataString = await AsyncStorage.getItem("auth"); 11 const authData = JSON.parse(authDataString || {}); 12 // Configure axios headers 13 configureAxiosHeaders(authData.token, authData.phone); 14 setAuthState(authData); 15 } catch (err) { 16 setAuthState({}); 17 } 18 }; 19 20 // Update AsyncStorage & context state 21 const setAuth = async (auth) => { 22 try { 23 await AsyncStorage.setItem("auth", JSON.stringify(auth)); 24 // Configure axios headers 25 configureAxiosHeaders(auth.token, auth.phone); 26 setAuthState(auth); 27 } catch (error) { 28 Promise.reject(error); 29 } 30 }; 31 32 useEffect(() => { 33 getAuthState(); 34 }, []); 35 36 return ( 37 <AuthContext.Provider value={{ auth, setAuth }}> 38 {children} 39 </AuthContext.Provider> 40 ); 41}; 42 43export { AuthContext, AuthProvider };
When using axios we can configure the default auth headers and they will be used for all API requests. In case you are not using axios you can still get auth values from the context.
1const configureAxiosHeaders = (token, phone) => { 2 axios.defaults.headers["X-Auth-Token"] = token; 3 axios.defaults.headers["X-Auth-Phone"] = phone; 4};
Now we can wrap our root component with AuthProvider and use the useContext() API in any component to access the auth state.
Restricting routes(screens)
Displaying appropriate screens and restricting access to screens based on the auth state is a common use case. In this section, we will see how to display Login/Signup screens if the user is not authenticated and a Home screen if the user is already authenticated.
1const App = () => { 2 // Get auth state from context 3 const { auth } = useContext(AuthContext); 4 5 return ( 6 <NavigationContainer> 7 <Stack.Navigator> 8 {auth.token ? ( 9 <Stack.Screen name="Home" component={Home} /> 10 ) : ( 11 <> 12 <Stack.Screen name="Signup" component={Signup} /> 13 <Stack.Screen name="Login" component={Login} /> 14 </> 15 )} 16 </Stack.Navigator> 17 </NavigationContainer> 18 ); 19};
In the above snippet, we are conditionally rendering screens based on the auth token rather than doing manual navigation using navigate function. This will help us restrict access to screens and avoid accessing restricted screens when navigating from code or using the system back button in android.
Things to keep in mind while using react navigation
-
Try to keep all top-level StackNavigator screens in one place to avoid unexpected behaviour.
-
Use conditional rendering to restrict screens.
-
Use replace instead of navigate when you want to remove the current screen from the back stack.
-
Use constants for screen names instead of string literals.
Enhancements
-
Store additional pieces of information like user type, username etc in the Auth context for easier access.
-
Improve AuthContext by using useReducer instead of useState.
-
Consider using state management libraries like Redux if you want to manage lots of states globally.
-
Use a loader state to display a SplashScreen until we fetch the auth state from AsyncStorage for the first time.