Build a Protected Auth Flow with React Navigation v6
Protect React Navigation screens by conditionally rendering Login or Home and re-validating tokens on focus with useFocusEffect.
03 Jul 2026, 07:50 UTC

Desired Outcome
Only authenticated users reach the Home screen. Unauthenticated attempts—including direct deep links—redirect to Login, and the app re-validates the token whenever Home regains focus without creating redirect loops.
Prerequisites
- React Native project with
@react-navigation/nativeand@react-navigation/native-stackversion 6 or later. - A token persistence layer such as AsyncStorage for development or
expo-secure-storefor production. - A login endpoint or mock function that returns an auth token.
Install the navigation packages
Run this in the project root:
npm install @react-navigation/native @react-navigation/native-stack
No special permissions are required. Replace npm with yarn if your project uses Yarn. Verify installation by checking node_modules/@react-navigation/native/package.json.
Procedure
Wrap the app once in NavigationContainer
import { NavigationContainer } from '@react-navigation/native'; import AuthProvider from './src/auth/AuthProvider'; import AppNavigator from './src/navigation/AppNavigator'; export default function App() { return ( ); }Render
NavigationContainerexactly once. Multiple containers create isolated navigation states.Create an authentication context
import { createContext, useContext, useState, useEffect, useCallback } from 'react'; import AsyncStorage from '@react-native-async-storage/async-storage'; export const AuthContext = createContext(null); export default function AuthProvider({ children }) { const [token, setToken] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { let mounted = true; (async () => { try { const stored = await AsyncStorage.getItem('authToken'); if (mounted) setToken(stored); } catch (error) { console.warn('Failed to read stored token', error); } finally { if (mounted) setLoading(false); } })(); return () => { mounted = false; }; }, []); const login = useCallback(async (newToken) => { try { await AsyncStorage.setItem('authToken', newToken); setToken(newToken); } catch (error) { console.warn('Failed to store token', error); throw error; } }, []); const logout = useCallback(async () => { try { await AsyncStorage.removeItem('authToken'); setToken(null); } catch (error) { console.warn('Failed to remove token', error); } }, []); return ( {children} ); }Define the Stack Navigator with conditional screens
import { createStackNavigator } from '@react-navigation/native-stack'; import { useContext } from 'react'; import AuthContext from '../auth/AuthProvider'; import LoginScreen from '../screens/LoginScreen'; import HomeScreen from '../screens/HomeScreen'; const Stack = createStackNavigator(); export default function AppNavigator() { const { token, loading } = useContext(AuthContext); if (loading) { return null; } return ( {!token ? ( ) : ( )} ); }Conditionally rendering one screen at a time keeps the navigation state consistent after token refresh or restart.
Validate the token on Home focus
import { useContext } from 'react'; import { useFocusEffect, useNavigation } from '@react-navigation/native'; import AuthContext from '../auth/AuthProvider'; export default function HomeScreen() { const { token, logout } = useContext(AuthContext); const navigation = useNavigation(); useFocusEffect( useCallback(() => { const validate = async () => { if (!token) { await logout(); navigation.replace('Login'); return; } }; validate().catch((error) => { console.warn('Token validation failed', error); logout(); navigation.replace('Login'); }); return () => { // Cleanup prevents pending validation from triggering state updates // after the screen unmounts. }; }, [token, logout, navigation]) ); return null; }Use
navigation.replace('Login')instead ofnavigateso the user cannot return to Home with the hardware back button. Always return a cleanup function fromuseFocusEffectto avoid infinite loops.Redirect after login
const handleLogin = async (newToken) => { await login(newToken); navigation.replace('Home'); };
Expected Checks
- Launch the app, log in with valid credentials, and confirm Home appears.
- Tap logout and confirm the app immediately replaces the screen with Login.
- Open the app with a deep link to Home while unauthenticated; confirm it renders Login.
- Set a short token expiry, leave Home, return after expiry, and confirm the redirect happens without console warnings about missing dependencies or cleanup.
Recovery Options
- Wrap every AsyncStorage call in
try/catch. On failure, show a retry button or fallback UI. - If token validation fails, call
logoutand replace the route with Login. - If
NavigationContainerfails to mount, render a simple error view instead of letting the app crash.
Limitations
- AsyncStorage is unencrypted. Use it only for development; production apps should use encrypted storage such as Expo SecureStore or react-native-keychain.
useFocusEffectruns after the screen focuses, so users may briefly see Home before the redirect. Combine it with the conditional initial screen above to minimize the flash.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.