L3: Fetch Access Token on Mount
Check for existing user session when app loads
Let's make the app check for an existing user session when it loads! This allows users to stay signed in even after closing the browser. 🔄
The Problem
Currently, when the app loads:
userisnulltokenisnullisLoadingistrue
But what if the user was previously signed in? They shouldn't have to sign in again every time!
Solution: Token Refresh on Mount
When the app loads, we'll:
- Call a refresh token API endpoint
- If valid token exists → Get user data
- If no token → User stays signed out
- Set
isLoadingtofalseeither way
This is called "session restoration" or "token refresh."
Understanding Token Refresh
Add Token Fetch Logic
Update AuthProvider to fetch the token when component mounts:
import { createContext, useContext, useState, useEffect } from 'react';
import api from '@/api';
const AuthContext = createContext(undefined);
export const useAuth = () => {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [token, setToken] = useState(null);
const [isLoading, setIsLoading] = useState(true);
// Fetch token on mount
useEffect(() => {
const fetchToken = async () => {
try {
// Call refresh endpoint (sends cookie automatically)
const response = await api.get('/auth/refresh');
// Extract token and user from response
const { accessToken, user: userData } = response.data;
// Update state
setToken(accessToken);
setUser(userData);
} catch (error) {
// No valid token - user not signed in
console.log('No valid session:', error.message);
} finally {
// Always set loading to false (whether success or error)
setIsLoading(false);
}
};
fetchToken();
}, []); // Empty dependency array = run once on mount
const signIn = async (email, password) => {
// TODO: Implement in later lesson
console.log('signIn called:', email);
};
const signOut = () => {
setUser(null);
setToken(null);
};
const value = {
user,
token,
isLoading,
signIn,
signOut,
};
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
};Understanding the Code
Run once on mount:
useEffect(() => {
const fetchToken = async () => {
// Async logic here
};
fetchToken();
}, []); // Empty array = run onceWhy this pattern?
1. Async function inside useEffect:
// ❌ Can't do this:
useEffect(async () => {
// ERROR: useEffect can't be async
}, []);
// ✅ Do this instead:
useEffect(() => {
const fetchData = async () => {
// Async logic
};
fetchData();
}, []);2. Empty dependency array:
useEffect(() => {
// Run once when component mounts
}, []); // No dependenciesWhen it runs:
AuthProvider mounts
↓
useEffect runs
↓
fetchToken() called
↓
API call made
↓
State updatedOnly runs once! Not on every render.
Three blocks explained:
try {
// Try to fetch token
const response = await api.get('/auth/refresh');
setToken(response.data.accessToken);
setUser(response.data.user);
} catch (error) {
// If API call fails (no token, network error, etc.)
console.log('No valid session:', error.message);
} finally {
// Always runs (whether success or error)
setIsLoading(false);
}Flow scenarios:
Scenario 1: Valid token exists
try block:
→ API call succeeds
→ setToken(...)
→ setUser(...)
finally block:
→ setIsLoading(false)
Result: User signed in ✅Scenario 2: No valid token
try block:
→ API call fails (401 Unauthorized)
→ Jump to catch block
catch block:
→ Log error
→ Don't update token/user (stay null)
finally block:
→ setIsLoading(false)
Result: User not signed in ✅Why finally is crucial:
// Without finally:
try {
// If this fails, isLoading stays true forever!
await api.get('/auth/refresh');
setIsLoading(false); // Never reached if error
} catch (error) {
// isLoading still true! App stuck in loading state
}
// With finally:
try {
await api.get('/auth/refresh');
} catch (error) {
// Error handled
} finally {
setIsLoading(false); // ✅ ALWAYS runs
}Refresh token endpoint:
const response = await api.get('/auth/refresh');What happens:
1. Browser sends request:
GET /auth/refresh HTTP/1.1
Cookie: refreshToken=abc123xyz ← Sent automatically2. Server verifies refresh token:
// Server-side
const refreshToken = req.cookies.refreshToken;
if (isValid(refreshToken)) {
// Generate new access token
const accessToken = generateAccessToken(user);
res.json({ accessToken, user });
}3. Response received:
{
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"id": "123",
"name": "John Doe",
"email": "john@example.com"
}
}4. Update state:
const { accessToken, user: userData } = response.data;
setToken(accessToken);
setUser(userData);Key point: Refresh token sent automatically in cookie. We don't handle it in JavaScript!
Two possible outcomes:
Success (valid token):
// Before
user: null
token: null
isLoading: true
// After successful fetch
user: { id, name, email }
token: "eyJhbGciOiJI..."
isLoading: falseFailure (no token):
// Before
user: null
token: null
isLoading: true
// After failed fetch
user: null ← Still null
token: null ← Still null
isLoading: false ← Changed!Why keep user/token null on failure?
// Don't create fake user:
catch (error) {
setUser({ error: true }); // ❌ Wrong!
}
// Keep null (represents "not signed in"):
catch (error) {
// Do nothing - let user/token stay null ✅
}null means: No user data available (either not signed in or session expired).
Testing the Token Fetch
Open browser DevTools (Network tab)
Refresh the page
You should see:
Request: GET /auth/refresh
Status: 401 Unauthorized (no token yet)This is expected! We haven't signed in yet.
Check console
You should see:
No valid session: Request failed with status code 401This confirms the try-catch is working.
Check React DevTools
AuthProvider state should be:
user: null
token: null
isLoading: false ← Changed from true!Loading State Flow
App Starts
↓
isLoading: true
↓
AuthProvider mounts
↓
useEffect runs
↓
fetchToken() called
↓
API call made
↓
Response received (or error)
↓
isLoading: false
↓
App ready!Timeline:
0ms: App starts (isLoading: true)
100ms: API call sent
300ms: Response received
301ms: State updated (isLoading: false)
302ms: Components re-render with final stateWhy This Matters
User experience:
Without token fetch:
1. User closes browser
2. Reopens app
3. Shows sign-in page
4. User annoyed: "I just signed in!" 😤
With token fetch:
1. User closes browser
2. Reopens app
3. Checks for refresh token
4. Restores session
5. User happy: "Still signed in!" 😊Real-world example:
Think of Gmail, Twitter, Facebook - you stay signed in across browser sessions. This is how they do it!
Common Issues
Problem: API call fails with network error
// Error in console
No valid session: Network ErrorCauses:
- Backend not running
- Wrong API URL
- CORS not configured
Solution: Check your api configuration:
import axios from 'axios';
const api = axios.create({
baseURL: 'http://localhost:3000', // Check this!
withCredentials: true, // Required for cookies
});
export default api;Problem: 404 Not Found
// Error in console
No valid session: Request failed with status code 404Cause: Endpoint doesn't exist on backend
Solution: Verify backend has /auth/refresh endpoint:
// Backend (Express example)
app.get('/auth/refresh', (req, res) => {
// Refresh token logic
});Problem: App stuck in loading state
// isLoading never becomes false
isLoading: true ← Forever!Cause: Missing finally block
// ❌ Bug:
try {
await api.get('/auth/refresh');
setIsLoading(false); // Only runs if success
} catch (error) {
// isLoading stays true!
}
// ✅ Fix:
try {
await api.get('/auth/refresh');
} catch (error) {
// Handle error
} finally {
setIsLoading(false); // Always runs ✅
}What's Next?
In Lesson 4, we'll:
- Use the
isLoadingstate to show/hide UI - Hide Navbar when user is not signed in
- Show loading state while checking session
- Improve user experience during app initialization
✅ Lesson Complete! Your app now checks for existing sessions on mount!
Key Takeaways
- ✅ Token refresh restores sessions across browser sessions
- ✅ useEffect with empty array runs once on mount
- ✅ Try-catch-finally handles success, error, and cleanup
- ✅ isLoading state prevents showing incorrect UI
- ✅ Refresh tokens stored in HTTP-only cookies (secure)
- ✅ Access tokens stored in memory (temporary)
- ✅ Session restoration provides seamless user experience