Il contesto
Dopo la migrazione di baccalaureat.sn verso Symfony + Turbo (raccontata in un articolo precedente), l'architettura è diventata: pagine Twig renderizzate lato server, e delle isole React per le zone interattive — il lettore PDF, il quiz, l'assistente IA. Ogni isola incorpora il proprio store Redux.
Il sintomo
L'utente effettua l'accesso sulla pagina iniziale: l'isola di autenticazione mostra correttamente il suo profilo. Naviga verso un'annata... e il lettore PDF lo tratta come visitatore anonimo: contenuti premium bloccati, preferiti assenti. Ritorno alla pagina iniziale: di nuovo connesso. Ogni pagina sembrava avere la propria opinione su chi fosse connesso.
La causa: uno store per ciclo di vita della pagina
La spiegazione sta in una frase che bisogna aver interiorizzato: uno store Redux vive nella memoria JavaScript della pagina, e Turbo sostituisce la pagina. Anche se Turbo evita il ricaricamento completo del browser, ogni navigazione sostituisce il <body> e rimonta le isole React della nuova pagina — che ricreano il loro store allo stato iniziale.
Page A (accueil) Page B (annale)
┌─────────────────────┐ Turbo ┌─────────────────────┐
│ îlot Auth │ ───────► │ îlot Lecteur PDF │
│ store #1 │ visite │ store #2 (NEUF) │
│ isLoggedIn: true │ │ isLoggedIn: false ← │
└─────────────────────┘ └─────────────────────┘
In una SPA classica, lo store viene creato una volta e sopravvive a tutte le «navigazioni» (che sono solo cambi di rotta lato client). Con Turbo, questo presupposto cade. Il mio codice veniva dal mondo SPA e presupponeva uno store immortale.
La falsa pista: persistere tutto lo store
Primo riflesso: redux-persist, per salvare e reidratare l'intero store a ogni pagina. Ho fatto marcia indietro rapidamente — persistere l'interezza dello stato (liste di prove, filtri, cache PDF) tra pagine che non hanno le stesse isole significa sincronizzare a forza mondi che non hanno nulla da dirsi, con i bug di reidratazione in più.
La soluzione: la fonte di verità fuori dallo store
La vera lezione di architettura: l'autenticazione non deve «vivere» in Redux quando le pagine sono effimere. Il JWT vive in localStorage — che, lui, sopravvive alle navigazioni — e ogni isola si idrata da questa fonte al montaggio:
// auth/bootstrap.js — partagé par tous les entry points
export function readAuthFromStorage() {
const token = localStorage.getItem('token');
const user = localStorage.getItem('user');
if (!token || !user) {
return { isLoggedIn: false, user: null, token: null };
}
return { isLoggedIn: true, user: JSON.parse(user), token };
}
// chaque îlot initialise SON store avec l'état préchargé
import { configureStore } from '@reduxjs/toolkit';
import { readAuthFromStorage } from '../auth/bootstrap';
const store = configureStore({
reducer: { auth: authReducer, lecteur: lecteurReducer },
preloadedState: {
auth: readAuthFromStorage(), // hydraté avant le premier rendu : zéro flash « déconnecté »
},
});
Il ruolo di Redux viene ridefinito: gestisce lo stato interno all'isola durante la vita della pagina (filtri del lettore, avanzamento del quiz). Lo stato che deve attraversare le pagine — la sessione — vive in localStorage, e lo store ne è solo uno specchio locale reidratato al montaggio.
Cosa mi porto dietro
- Con Turbo, ogni navigazione ricrea le isole React e i loro store: ogni stato Redux è effimero per costruzione.
- La questione di architettura non è «come far sopravvivere lo store» ma «quale stato deve realmente sopravvivere» — e quello stato vive in localStorage, non in Redux.
preloadedStateidrata lo store prima del primo render: nessun flash «disconnesso» né useEffect di recupero.- Le abitudini del mondo SPA (store immortale, stato globale unico) non si trasferiscono tali e quali in un'architettura a isole — ogni paradigma ha i suoi invarianti.