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 AuthContext met user, token en functies login, register, logout.
  • Een Login en Register pagina die die functies gebruiken.
  • Token bewaren in localStorage zodat 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);
}
Wat zit er in de context?
  • user — info over de ingelogde gebruiker (of null).
  • token — de JWT die je meestuurt in Authorization headers.
  • login / register — async functies die je backend aanroepen en bij succes state + localStorage vullen.
  • logout — wist alles, lokaal én in localStorage.
  • useAuth() — eigen hook die useContext wrapped, 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;
Wat valt op?
  • Geen fetch in dit component — die zit in AuthContext.
  • Het component kent alleen useAuth().login en error.
  • Bij throw in login belandt de error in de catch hier 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