Zustand per Gestione Stato Autenticazione β
π― PerchΓ© Zustand? β
Abbiamo sostituito l'uso diretto di localStorage con Zustand per ottenere:
β State Management Centralizzato - Un unico source of truth per l'autenticazione β Persist Automatico - Sincronizzazione automatica con localStorage β Cross-Tab Sync - Sincronizzazione nativa tra tab del browser β Type Safety - TypeScript completo β Performance - Re-render ottimizzati con selectors β DevTools - Supporto per Redux DevTools β Zero Boilerplate - API semplice e minimale
π¦ Installazione β
npm install zustandποΈ Struttura Store β
File: app/store/auth/auth.store.ts β
import { create } from "zustand";
import { persist, createJSONStorage } from "zustand/middleware";
interface AuthState {
// Stato
isAuthenticated: boolean;
loginData: LoginData | null;
// Actions
login: (data: LoginData) => void;
logout: () => void;
setLoginData: (data: LoginData | null) => void;
}
export const useAuthStore = create<AuthState>()(
persist(
(set) => ({
isAuthenticated: false,
loginData: null,
login: (data) => set({ isAuthenticated: true, loginData: data }),
logout: () => set({ isAuthenticated: false, loginData: null }),
setLoginData: (data) => set({ loginData: data }),
}),
{
name: "auth-storage", // Chiave in localStorage
storage: createJSONStorage(() => localStorage),
}
)
);πΎ Persist Middleware β
Il middleware persist di Zustand:
- Salva automaticamente lo stato in localStorage ad ogni cambiamento
- Ripristina lo stato al caricamento della pagina
- Sincronizza automaticamente tra tab (cross-tab sync nativo)
localStorage prima (β Manuale): β
// Devi gestire manualmente
localStorage.setItem("auth", "authenticated");
localStorage.setItem("loginData", JSON.stringify(data));
// Devi parsare manualmente
const auth = localStorage.getItem("auth") === "authenticated";
const data = JSON.parse(localStorage.getItem("loginData") || "null");Zustand persist (β Automatico): β
// Salvataggio automatico
useAuthStore.getState().login(data);
// Accesso diretto tipizzato
const { isAuthenticated, loginData } = useAuthStore.getState();π¨ Utilizzo nei Componenti β
1. Accesso Base β
import { useAuthStore } from "@/store/auth/auth.store";
function MyComponent() {
// β οΈ Si ri-renderizza ad ogni cambio dello store
const { isAuthenticated, loginData, login, logout } = useAuthStore();
return <div>{isAuthenticated ? "Logged in" : "Logged out"}</div>;
}2. Con Selectors (β Raccomandato) β
function MyComponent() {
// β
Si ri-renderizza solo quando isAuthenticated cambia
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
return <div>{isAuthenticated ? "Logged in" : "Logged out"}</div>;
}3. Solo Actions (β Zero Re-render) β
function LogoutButton() {
// β
Questo componente non si ri-renderizza mai
const logout = useAuthStore((state) => state.logout);
return <button onClick={logout}>Logout</button>;
}4. Selectors Predefiniti β
import { useAuthStore, selectIsAuthenticated } from "@/store/auth/auth.store";
function MyComponent() {
const isAuthenticated = useAuthStore(selectIsAuthenticated);
// Equivalente a: useAuthStore((state) => state.isAuthenticated)
}π§ Utilizzo fuori da React β
In authService β
// Prima (con localStorage)
isAuthenticated(): boolean {
return localStorage.getItem("auth") === "authenticated";
}
// Dopo (con Zustand)
isAuthenticated(): boolean {
return useAuthStore.getState().isAuthenticated;
}In Utility Functions β
export function checkAuth() {
const { isAuthenticated, loginData } = useAuthStore.getState();
if (!isAuthenticated) {
console.log("Not authenticated");
return false;
}
console.log("Authenticated with redirect:", loginData?.redirect);
return true;
}π Cross-Tab Synchronization β
Zustand con persist sincronizza automaticamente tra tab:
// Tab 1: Login
useAuthStore.getState().login({ redirect: "main" });
// Tab 2: Stato aggiornato automaticamente! β¨
const isAuth = useAuthStore.getState().isAuthenticated; // trueCome Funziona β
- Zustand usa
storageevents di localStorage - Quando una tab modifica lo stato, localStorage cambia
- Altre tab ricevono l'evento e aggiornano il loro stato
- Tutto automatico! No BroadcastChannel necessario
BroadcastChannel vs Zustand Persist β
Prima (con BroadcastChannel):
// Devi gestire manualmente i messaggi
const authChannel = new BroadcastChannel("auth");
// Tab 1: Invia messaggio
localStorage.setItem("auth", "authenticated");
authChannel.postMessage({ type: "login" });
// Tab 2: Ricevi messaggio
authChannel.onmessage = (e) => {
if (e.data.type === "login") {
localStorage.setItem("auth", "authenticated");
navigate("/welcome");
}
};Dopo (con Zustand persist):
// Tab 1: Login
useAuthStore.getState().login(data);
// Tab 2: Niente da fare! π
// Lo stato Γ¨ giΓ sincronizzato automaticamenteπ Subscribe a Cambiamenti β
Per eseguire side effects quando lo stato cambia:
// Setup listener
const unsubscribe = useAuthStore.subscribe(
(state) => {
console.log("Auth changed:", state.isAuthenticated);
if (state.isAuthenticated) {
// Esegui azioni dopo login
console.log("User logged in!");
}
}
);
// Cleanup
unsubscribe();In un componente React:
useEffect(() => {
const unsubscribe = useAuthStore.subscribe((state) => {
console.log("Auth state:", state.isAuthenticated);
});
return () => unsubscribe();
}, []);π― Best Practices β
β DO β
// 1. Usa selectors per performance
const isAuth = useAuthStore((state) => state.isAuthenticated);
// 2. Accedi alle actions direttamente
const login = useAuthStore((state) => state.login);
// 3. Fuori da React, usa getState()
const auth = useAuthStore.getState().isAuthenticated;
// 4. Definisci selectors riutilizzabili
export const selectIsAuth = (state: AuthState) => state.isAuthenticated;
const isAuth = useAuthStore(selectIsAuth);β DON'T β
// 1. Evita di prendere tutto lo store
const { isAuth, login, logout, loginData } = useAuthStore(); // β
// 2. Non usare useAuthStore() fuori da React
function myFunction() {
const store = useAuthStore(); // β Errore!
}
// 3. Non creare selectors inline inutili
useAuthStore((state) => state); // β Re-render ad ogni cambioπ Confronto: Prima vs Dopo β
Prima (localStorage manuale) β
Problemi:
- β Codice ripetitivo (setItem/getItem ovunque)
- β Parsing JSON manuale
- β Nessuna type safety
- β Sincronizzazione cross-tab complicata
- β State sparso in multiple chiavi
// Login
localStorage.setItem("auth", "authenticated");
localStorage.setItem("loginData", JSON.stringify(data));
// Check
const isAuth = localStorage.getItem("auth") === "authenticated";
// Get data
const data = JSON.parse(localStorage.getItem("loginData") || "null");
// Logout
localStorage.removeItem("auth");
localStorage.removeItem("loginData");Dopo (Zustand) β
Vantaggi:
- β API semplice e centralizzata
- β Type safety completo
- β Persist e sync automatici
- β Un unico source of truth
- β Performance ottimizzate
// Login
useAuthStore.getState().login(data);
// Check
const isAuth = useAuthStore.getState().isAuthenticated;
// Get data
const data = useAuthStore.getState().loginData;
// Logout
useAuthStore.getState().logout();π§ͺ Testing β
Il mock di localStorage in tests/setup.ts funziona automaticamente con Zustand:
// tests/setup.ts
const localStorageMock = { ... };
Object.defineProperty(global, "localStorage", { value: localStorageMock });Nei test:
### Esempio Completo
```typescript
import { useAuthStore } from "@/store/auth/auth.store";
it("should login user", () => {
// Reset store
useAuthStore.getState().logout();
// Test login
useAuthStore.getState().login({ redirect: "main" });
expect(useAuthStore.getState().isAuthenticated).toBe(true);
});π οΈ Redux DevTools β
Zustand supporta Redux DevTools per debugging:
import { devtools, persist } from "zustand/middleware";
export const useAuthStore = create<AuthState>()(
devtools(
persist(
(set) => ({ ...state }),
{ name: "auth-storage" }
),
{ name: "AuthStore" }
)
);Poi installa l'estensione Redux DevTools nel browser e vedrai lo store!
π Migrazione Completa β
File Modificati β
1. app/lib/auth.ts β
// Prima
localStorage.setItem("auth", "authenticated");
localStorage.setItem("loginData", JSON.stringify(data));
// Dopo
useAuthStore.getState().login(data);2. app/lib/storage.ts β
// Prima
export function getLoginData(): LoginData | null {
const data = localStorage.getItem("loginData");
return data ? JSON.parse(data) : null;
}
// Dopo
export function getLoginData(): LoginData | null {
return useAuthStore.getState().loginData;
}π Risorse β
β¨ Conclusione β
Zustand ha semplificato:
- β -50% di codice per gestione stato
- β Zero configurazione per persist
- β Sync automatico cross-tab
- β Type safety completa
- β Performance migliorate con selectors
Da localStorage manuale a Zustand in una riga:
const { isAuthenticated, login, logout } = useAuthStore();π State management semplificato e potente!