Authentication navigation controls which screens a user can access based on their authentication state. In a React Native application, we can use Auth Context to decide whether to show login screens or protected application screens.
Authentication navigation means showing different navigation flows depending on whether the user is authenticated.
Not Logged In
↓
Login / Registration
Logged In
↓
Home / Profile / Dashboard
A mobile application may have public screens and protected screens. Authentication navigation provides a clean way to separate them.
| Public Screens | Protected Screens |
|---|---|
| Login | Home |
| Registration | Profile |
| Forgot Password | Student Dashboard |
App Starts
↓
Check Authentication
↓
Loading?
↓
Yes → Loading Screen
↓
No
↓
Authenticated?
↙ ↘
Yes No
↓ ↓
App Auth
Navigator Navigator
interface AuthContextType {
user: User | null;
token: string | null;
isAuthenticated: boolean;
loading: boolean;
login: (
token: string,
user: User
) => Promise<void>;
logout: () => Promise<void>;
}
Navigation can use isAuthenticated and
loading to decide which navigator to display.
const AuthNavigator = () => {
return (
<Stack.Navigator>
<Stack.Screen
name="Login"
component={LoginScreen}
/>
<Stack.Screen
name="Register"
component={RegisterScreen}
/>
</Stack.Navigator>
);
};
const AppNavigator = () => {
return (
<Stack.Navigator>
<Stack.Screen
name="Home"
component={HomeScreen}
/>
<Stack.Screen
name="Profile"
component={ProfileScreen}
/>
</Stack.Navigator>
);
};
const RootNavigator = () => {
const {
isAuthenticated,
loading
} = useAuth();
if (loading) {
return <LoadingScreen />;
}
if (isAuthenticated) {
return <AppNavigator />;
}
return <AuthNavigator />;
};
const LoadingScreen = () => {
return (
<View>
<ActivityIndicator
size="large"
/>
<Text>
Checking authentication...
</Text>
</View>
);
};
The loading screen prevents the application from immediately showing the wrong navigation flow while the stored authentication session is being checked.
When the application starts, Auth Context may need to read the stored token from AsyncStorage and verify the current session.
App Starts
↓
loading = true
↓
Read Token
↓
Restore User
↓
loading = false
↓
Show Correct Navigator
const handleLogin = async () => {
const response =
await api.post(
"/login.php",
{
email,
password
}
);
await login(
response.data.token,
response.data.user
);
};
Updating the Auth Context can cause the root navigation to switch to the authenticated application flow.
const handleLogout = async () => {
await logout();
};
After the authentication state becomes unauthenticated, the root navigation can display the Auth Navigator again.
const ProfileScreen = () => {
const {
user,
logout
} = useAuth();
return (
<View>
<Text>
{user?.name}
</Text>
<Button
title="Logout"
onPress={logout}
/>
</View>
);
};
A user can modify a mobile application or send requests directly to the API. Therefore, hiding a screen is not a security mechanism for the backend.
React Native Navigation
↓
User Experience
PHP API
↓
Actual Security
The PHP server must verify the JWT independently.
Authenticated User
↓
App Navigator
↓
Profile Screen
↓
Axios Request
↓
Bearer JWT
↓
PHP Protected API
↓
JWT Verification
import {
createNativeStackNavigator
} from "@react-navigation/native-stack";
const Stack =
createNativeStackNavigator();
The native stack navigator can be used for both authenticated and unauthenticated navigation flows.
const AuthNavigator = () => {
return (
<Stack.Navigator>
<Stack.Screen
name="Login"
component={LoginScreen}
/>
<Stack.Screen
name="Register"
component={RegisterScreen}
/>
<Stack.Screen
name="ForgotPassword"
component={ForgotPasswordScreen}
/>
</Stack.Navigator>
);
};
const AppNavigator = () => {
return (
<Stack.Navigator>
<Stack.Screen
name="Home"
component={HomeScreen}
/>
<Stack.Screen
name="Profile"
component={ProfileScreen}
/>
<Stack.Screen
name="Students"
component={StudentListScreen}
/>
</Stack.Navigator>
);
};
const RootNavigator = () => {
const {
loading,
isAuthenticated
} = useAuth();
if (loading) {
return <LoadingScreen />;
}
return isAuthenticated
? <AppNavigator />
: <AuthNavigator />;
};
const App = () => {
return (
<NavigationContainer>
<AuthProvider>
<RootNavigator />
</AuthProvider>
</NavigationContainer>
);
};
AuthProvider must wrap the components that need access to authentication state.
Login Screen
↓
POST /login.php
↓
JWT + User
↓
login()
↓
isAuthenticated = true
↓
RootNavigator
↓
AppNavigator
↓
Home Screen
The navigation flow can change automatically when the context state changes.
Profile Screen
↓
logout()
↓
Remove Token
↓
Clear User
↓
isAuthenticated = false
↓
RootNavigator
↓
AuthNavigator
↓
Login Screen
If a protected API returns HTTP 401 because the token is no longer accepted, the authentication state can be cleared.
if (
axios.isAxiosError(error) &&
error.response?.status === 401
) {
await logout();
}
The root navigator can then show the authentication flow.
The user role can influence which screens are displayed.
if (user?.role === "admin") {
return <AdminDashboard />;
}
if (user?.role === "teacher") {
return <TeacherDashboard />;
}
return <StudentDashboard />;
This controls the UI. The server must still enforce role-based permissions.
const AdminScreen = () => {
const { user } = useAuth();
if (user?.role !== "admin") {
return (
<Text>
Access denied
</Text>
);
}
return (
<View>
<Text>
Admin Dashboard
</Text>
</View>
);
};
This is a UI-level check. The corresponding admin API must also check the user's role on the server.
const RootNavigator = () => {
const {
loading,
isAuthenticated,
user
} = useAuth();
if (loading) {
return <LoadingScreen />;
}
if (!isAuthenticated) {
return <AuthNavigator />;
}
if (user?.role === "admin") {
return <AdminNavigator />;
}
if (user?.role === "teacher") {
return <TeacherNavigator />;
}
return <AppNavigator />;
};
const restoreAuth =
async () => {
const token =
await AsyncStorage.getItem(
"authToken"
);
if (!token) {
setLoading(false);
return;
}
try {
const response =
await api.get<UserResponse>(
"/profile.php"
);
setToken(token);
setUser(
response.data.user
);
} catch {
await AsyncStorage.removeItem(
"authToken"
);
setToken(null);
setUser(null);
} finally {
setLoading(false);
}
};
The stored token can be checked against a protected API before the application considers the session restored.
src/
│
├── context/
│ └── AuthContext.tsx
│
├── navigation/
│ ├── RootNavigator.tsx
│ ├── AuthNavigator.tsx
│ └── AppNavigator.tsx
│
├── screens/
│ ├── LoginScreen.tsx
│ ├── RegisterScreen.tsx
│ ├── HomeScreen.tsx
│ └── ProfileScreen.tsx
│
├── services/
│ └── api.ts
│
└── types/
└── auth.ts
Separating context, navigation, screens, services, and types makes the project easier to maintain.
App.tsx
↓
NavigationContainer
↓
AuthProvider
↓
Restore Token
↓
Loading Screen
↓
Check Authentication
↓
┌──────────────────────┐
│ │
Authenticated Not Authenticated
│ │
↓ ↓
App Navigator Auth Navigator
│ │
↓ ↓
Home/Profile Login/Register
│
↓
Axios
│
↓
Bearer JWT
│
↓
PHP Protected API
│
↓
JWT Verification
│
↓
MySQL
Auth Context, authentication navigation, Axios, JWT, and the PHP protected API work together to create a complete authentication architecture for a React Native application.
Question: What should a React Native app normally show when the user is not authenticated?