Het patroon
De backend stuurt na een succesvolle login of register een JWT-token terug. Die token heeft de frontend nodig om beveiligde routes te bereiken. Probleem: meerdere componenten hebben de token nodig — de lijst-pagina, de form, de logout-knop in de header.
Oplossing: Context API. Eén plek waar de huidige user en token leven, overal toegankelijk
via useContext.
Wat ga je bouwen
- Een
AuthContextmetuser,tokenen functieslogin,register,logout. - Een
LoginenRegisterpagina die die functies gebruiken. - Token bewaren in
localStoragezodat je ingelogd blijft na refresh.
AuthContext aanmaken
Maak frontend/src/context/AuthContext.jsx:
// frontend/src/context/AuthContext.jsx
import { createContext, useContext, useState } from 'react';
const AuthContext = createContext(null);
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [token, setToken] = useState(null);
const login = async (email, password) => {
const response = await fetch('http://localhost:4000/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
const data = await response.json();
if (!response.ok) throw new Error(data.error);
setUser({ email: data.email });
setToken(data.token);
localStorage.setItem('token', data.token);
localStorage.setItem('email', data.email);
};
const register = async (email, password) => {
const response = await fetch('http://localhost:4000/api/auth/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
const data = await response.json();
if (!response.ok) throw new Error(data.error);
setUser({ email: data.email });
setToken(data.token);
localStorage.setItem('token', data.token);
localStorage.setItem('email', data.email);
};
const logout = () => {
setUser(null);
setToken(null);
localStorage.removeItem('token');
localStorage.removeItem('email');
};
return (
<AuthContext.Provider value={{ user, token, login, register, logout }}>
{children}
</AuthContext.Provider>
);
}
// Handige hook
export function useAuth() {
return useContext(AuthContext);
}
user— info over de ingelogde gebruiker (ofnull).token— de JWT die je meestuurt inAuthorizationheaders.login/register— async functies die je backend aanroepen en bij succes state + localStorage vullen.logout— wist alles, lokaal én in localStorage.useAuth()— eigen hook dieuseContextwrapped, scheelt boilerplate in elk component.
Provider om je app heen
Wikkel je hele app in AuthProvider zodat elk component erbij kan:
// frontend/src/main.jsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { AuthProvider } from './context/AuthContext';
ReactDOM.createRoot(document.getElementById('root')).render(
<AuthProvider>
<App />
</AuthProvider>
);
Vanaf nu overal beschikbaar
In elk component kun je const { user, token, logout } = useAuth(); doen. Geen props
doorgeven door tien componenten heen.
Login pagina
// frontend/src/pages/Login.jsx
import { useState } from 'react';
import { useAuth } from '../context/AuthContext';
function Login() {
const { login } = useAuth();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState(null);
const handleSubmit = async (e) => {
e.preventDefault();
setError(null);
try {
await login(email, password);
// gelukt — eventueel navigeren naar /
} catch (err) {
setError(err.message);
}
};
return (
<form onSubmit={handleSubmit}>
<h2>Inloggen</h2>
<label>Email:</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<label>Wachtwoord:</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button type="submit">Inloggen</button>
{error && <p className="error">{error}</p>}
</form>
);
}
export default Login;
- Geen
fetchin dit component — die zit inAuthContext. - Het component kent alleen
useAuth().loginenerror. - Bij
throwinloginbelandt de error in decatchhier en tonen we hem aan de gebruiker.
Register pagina
Identieke structuur als Login, maar met register uit de context:
// frontend/src/pages/Register.jsx
import { useState } from 'react';
import { useAuth } from '../context/AuthContext';
function Register() {
const { register } = useAuth();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState(null);
const handleSubmit = async (e) => {
e.preventDefault();
setError(null);
try {
await register(email, password);
} catch (err) {
setError(err.message);
}
};
return (
<form onSubmit={handleSubmit}>
<h2>Account aanmaken</h2>
<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">Registreren</button>
{error && <p className="error">{error}</p>}
</form>
);
}
export default Register;
Logout
Een knop ergens in je layout (bijv. in een header):
// frontend/src/components/Header.jsx
import { useAuth } from '../context/AuthContext';
function Header() {
const { user, logout } = useAuth();
return (
<header>
<h1>Workouts</h1>
{user && (
<div>
<span>{user.email}</span>
<button onClick={logout}>Uitloggen</button>
</div>
)}
</header>
);
}
Conditioneel renderen
De logout-knop verschijnt alleen als er een user is. Niet ingelogd? Dan zie je hem niet.
Ingelogd blijven na refresh
Na F5 is je React-state weg. Maar localStorage blijft. Bij het mounten van de
AuthProvider check je daar even of er nog een token staat:
// In AuthProvider, naast je bestaande useState's
import { useEffect } from 'react';
useEffect(() => {
const savedToken = localStorage.getItem('token');
const savedEmail = localStorage.getItem('email');
if (savedToken && savedEmail) {
setToken(savedToken);
setUser({ email: savedEmail });
}
}, []);
Resultaat
Refresh de pagina — je blijft ingelogd. Sluit de browser, open hem morgen weer — nog steeds ingelogd (totdat de token verloopt aan de backend-kant).
Veelgemaakte fouten
1. Token in sessionStorage ipv localStorage
sessionStorage wordt gewist als je de tab sluit. Als je wil dat de gebruiker ingelogd
blijft, gebruik localStorage.
2. Vergeten AuthProvider in main.jsx te wrappen
Krijg je Cannot read property 'login' of null. De provider moet boven elk component staan
dat useAuth() aanroept.
3. Geen error tonen bij verkeerde inlog
Backend geeft 401 terug, maar je form doet niets. Vang errors altijd in try/catch en toon
ze.
4. Token in code hardcoden tijdens testen
Verleidelijk om snel een token te plakken in een fetch — maar dan vergeet je hem ergens en commit je een geldige token in Git. Liever altijd via context.
Volgende Stap
Login en register werken. Nu de token gebruiken om beveiligde routes te bereiken én pagina's afschermen voor niet-ingelogde users.
Protected Routes & Bearer Token →
Token meesturen in fetches en pagina's beveiligen