An Auth Context and Login Form
An Auth Context and Login Form
~16 Min. Lesezeit Zuletzt aktualisiert am August 8, 2026
The token from chapter 49 needs to be managed CENTRALLY, ACCESSIBLE to EVERY component that needs to know WHETHER and AS WHOM the user is logged in – EXACTLY the use case from chapter 73 for React Context.
Creating the AuthContext
import { createContext, useContext, useState, type ReactNode } from 'react';
interface AuthContextValue {
token: string | null;
login: (token: string) => void;
logout: () => void;
}
const AuthContext = createContext<AuthContextValue | null>(null);
export function AuthProvider({ children }: { children: ReactNode }) {
const [token, setToken] = useState<string | null>(
() => localStorage.getItem('token'),
);
function login(newToken: string) {
localStorage.setItem('token', newToken);
setToken(newToken);
}
function logout() {
localStorage.removeItem('token');
setToken(null);
}
return (
<AuthContext.Provider value={{ token, login, logout }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
}Achtung: localStorage is the SIMPLEST solution, but VULNERABLE to XSS (chapter 56 previewed this trade-off) – SUFFICIENT for OUR learning project, for a PRODUCTION system an HTTP-only cookie (issued by the backend) would be the SAFER pattern, though THEN requiring additional CSRF protection.
Wiring up the provider
// main.tsx - AuthProvider WRAPS App, INSIDE QueryClientProvider
<QueryClientProvider client={queryClient}>
<AuthProvider>
<App />
</AuthProvider>
</QueryClientProvider>The login form
import { useState, type FormEvent } from 'react';
import { useMutation } from '@tanstack/react-query';
import { apiClient } from '../api/client';
import { useAuth } from '../context/AuthContext';
function LoginPage() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const { login } = useAuth();
const loginMutation = useMutation({
mutationFn: async () => {
const response = await apiClient.post<{ token: string }>('/login', {
email,
password,
});
return response.data;
},
onSuccess: (data) => {
login(data.token);
},
});
function handleSubmit(event: FormEvent) {
event.preventDefault();
loginMutation.mutate();
}
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button type="submit" disabled={loginMutation.isPending}>
Log in
</button>
{loginMutation.isError && <p>Login failed</p>}
</form>
);
}
export default LoginPage;useMutation instead of useQuery – EXACTLY as the Symfony course distinguishes forms between DISPLAYING (GET) and CHANGING (POST), TanStack Query distinguishes between READING (useQuery) and WRITING (useMutation) operations.
Tipp: apiClient.post('/login', ...) uses the Content-Type: application/ld+json header from chapter 74 – but the login endpoint (chapter 49) expects PLAIN JSON. In practice this MOSTLY still works (Symfony is TOLERANT), a cleaner approach would be a SEPARATE Content-Type override for ONLY this call.