Authentication in a React App
AdvancedWhere the token lives, how the app knows who you are before the first render, and what happens on a 401. The security decisions here outweigh the React ones.
Overview
Auth is where the frontend and backend meet, and it is a favourite interview topic because the trade-offs are real. The first decision — an httpOnly cookie or a token held in JavaScript — is a security decision the React code then has to live with. After that come the React-shaped problems: avoiding a flash of the signed-out UI while the session is being checked, refreshing an expired token without ten parallel requests all triggering their own refresh, and clearing every trace of the previous user on sign-out.
Where the Session Lives
Two designs, and the consequences that follow from each.
// Option A — httpOnly cookie set by the server (the stronger default)
// + JavaScript cannot read it, so an XSS cannot steal it
// - needs SameSite and CSRF protection
// - needs credentials: 'include' for cross-origin calls
fetch('/api/me', { credentials: 'include' })
// Option B — access token in memory, refresh token in an httpOnly cookie
// + works cleanly across separate API domains and mobile clients
// + the access token is never persisted, so a reload drops it
// - more moving parts
// Anti-pattern: the token in localStorage. Any script on the page can
// read it, so one XSS — including one inside a dependency — hands over
// every session.
// And never decode a JWT on the client and trust it for authorisation.
// Read it for display (a name, an expiry) only; the server decides
// what the user is allowed to do.Three States, Not Two
The third state is what prevents a flash of the signed-out UI on every reload.
// Wrong: user is either present or absent, so on the first render
// it is null and the app renders the sign-in page for a moment
// before the session check returns.
const AuthContext = createContext(null)
export function AuthProvider({ children }) {
const { data: user, isLoading } = useQuery({
queryKey: ['session'],
queryFn: getMe, // 401 -> null, not an error
retry: false,
staleTime: Infinity,
})
if (isLoading) return <FullPageSpinner /> // the third state
return (
<AuthContext.Provider value={{ user, isAuthenticated: !!user }}>
{children}
</AuthContext.Provider>
)
}
export function useAuth() {
const ctx = useContext(AuthContext)
if (!ctx) throw new Error('useAuth must be used inside <AuthProvider>')
return ctx
}
// checking | signed-in | signed-out. Rendering anything conditional
// on auth before the check completes is what causes the flash.401 Handling and Sign-Out
One refresh for many concurrent failures, and a genuinely clean sign-out.
// Ten requests fail with 401 at once. Without care, ten refreshes.
let refreshing = null
async function apiFetch(url, options = {}) {
let res = await fetch(url, { ...options, credentials: 'include' })
if (res.status === 401 && !options._retried) {
refreshing ??= refresh().finally(() => { refreshing = null }) // share one
const ok = await refreshing
if (!ok) { redirectToLogin(); throw new AuthError() }
res = await apiFetch(url, { ...options, _retried: true })
}
return res
}
// Sign-out must clear the cache, or the next user sees the previous
// user's data on screen before the refetch lands.
async function signOut() {
await post('/api/logout') // server clears the cookie
queryClient.clear() // drop every cached response
navigate('/login', { replace: true })
}
// Also worth handling: a session that expires in another tab.
// The storage event or a periodic session check keeps tabs in sync.Key Points to Remember
- 1An httpOnly cookie cannot be read by JavaScript and is the stronger default; localStorage exposes the token to any XSS
- 2Never trust a client-decoded JWT for authorisation — the server decides what a user may do
- 3Model auth as checking, signed-in and signed-out, or the app flashes the signed-out UI on every reload
- 4Concurrent 401s must share a single in-flight refresh rather than each triggering their own
- 5Sign-out has to clear the query cache, or the next user briefly sees the previous user's data
Interview Questions
Sign in to ask AriaWhere should an auth token be stored in a browser app, and why?
How do you avoid a flash of the signed-out UI while the session is being verified?
Ten requests fail with 401 simultaneously. How do you avoid ten refresh calls?
Ask Aria about Authentication in a React App
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.