Skip to content

State Management - Auth Store

Guida pratica all'utilizzo dello store Zustand per l'autenticazione.

Panoramica

Lo store useAuthStore gestisce lo stato di autenticazione dell'applicazione:

ProprietàTipoDescrizione
isAuthenticatedbooleanL'utente è autenticato?
loginDataLoginData | nullDati del login (redirect, module, ecc.)
userSettingsUserSettings | nullImpostazioni utente e azienda selezionata

Lo stato viene persistito automaticamente in localStorage e sincronizzato tra tab.


Quick Start

Leggere lo Stato

typescript
import { useAuthStore } from "@/store/auth/auth.store";

// Nei componenti React - usa selectors
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
const userSettings = useAuthStore((state) => state.userSettings);

// Fuori da React - usa getState()
const isAuth = useAuthStore.getState().isAuthenticated;

Modificare lo Stato

typescript
// Nei componenti React
const login = useAuthStore((state) => state.login);
login(loginData);

// Fuori da React
useAuthStore.getState().login(loginData);
useAuthStore.getState().logout();

Utilizzo nei Componenti React

Pattern Selectors (Raccomandato)

Usa sempre selectors per ottimizzare i re-render:

typescript
import { useAuthStore } from "@/store/auth/auth.store";

function UserInfo() {
    // ✅ Ri-renderizza SOLO quando isAuthenticated cambia
    const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
    const userSettings = useAuthStore((state) => state.userSettings);

    if (!isAuthenticated) {
        return <div>Non autenticato</div>;
    }

    return <div>Benvenuto, {userSettings?.user?.name}</div>;
}

Accesso alle Actions

typescript
function LogoutButton() {
    // ✅ Questo componente non si ri-renderizza mai
    // (le actions non cambiano)
    const logout = useAuthStore((state) => state.logout);

    return <button onClick={logout}>Logout</button>;
}

Anti-pattern da Evitare

typescript
// ❌ SBAGLIATO - Ri-renderizza ad ogni cambio dello store
const { isAuthenticated, loginData, logout } = useAuthStore();

// ✅ CORRETTO - Seleziona solo ciò che serve
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
const logout = useAuthStore((state) => state.logout);

Utilizzo Fuori da React

Per accedere allo store in utility functions, services, o loaders:

Leggere lo Stato

typescript
import { useAuthStore } from "@/store/auth/auth.store";

export function isUserAuthenticated(): boolean {
    return useAuthStore.getState().isAuthenticated;
}

export function getCurrentUser(): UserSettings | null {
    return useAuthStore.getState().userSettings;
}

Modificare lo Stato

typescript
import { useAuthStore } from "@/store/auth/auth.store";

export function performLogin(data: LoginData) {
    useAuthStore.getState().login(data);
}

export function performLogout() {
    useAuthStore.getState().logout();
}

Esempio: authService

typescript
// app/lib/auth/auth.ts
import { useAuthStore } from "@/store/auth/auth.store";

export const authService = {
    isAuthenticated(): boolean {
        return useAuthStore.getState().isAuthenticated;
    },

    login(data: LoginData) {
        useAuthStore.getState().login(data);
    },

    logout() {
        useAuthStore.getState().logout();
    },
};

Sincronizzazione Cross-Tab

Sincronizzazione Automatica (Zustand Persist)

Lo stato si sincronizza automaticamente tra tutte le tab del browser:

typescript
// Tab 1: Login
useAuthStore.getState().login(data);

// Tab 2: Stato aggiornato automaticamente!
const isAuth = useAuthStore.getState().isAuthenticated; // true

Questo funziona grazie al middleware persist che usa gli eventi di localStorage.

BroadcastChannel (per Navigazione)

Per azioni che richiedono navigazione coordinata tra tab, usa AuthSyncListener:

typescript
// Il componente AuthSyncListener (in root.tsx) gestisce:
// - logout → tutte le tab navigano a /
// - login → tutte le tab si aggiornano
// - company_selected → tutte le tab navigano al modulo
EventoEffetto
loginSincronizza stato + naviga
logoutPulisce stato + naviga a login
company_selectedCarica settings + naviga al modulo
back_to_companiesNaviga a /companies

Subscribe a Cambiamenti

Per eseguire side effects quando lo stato cambia:

Pattern Base

typescript
const unsubscribe = useAuthStore.subscribe((state) => {
    console.log("Auth changed:", state.isAuthenticated);
});

// Cleanup
unsubscribe();

In un Componente React

typescript
import { useEffect } from "react";
import { useAuthStore } from "@/store/auth/auth.store";

function AuthLogger() {
    useEffect(() => {
        const unsubscribe = useAuthStore.subscribe((state) => {
            console.log("Auth state:", state.isAuthenticated);
        });

        return () => unsubscribe();
    }, []);

    return null;
}

Subscribe con Selector

typescript
// Esegui callback solo quando isAuthenticated cambia
const unsubscribe = useAuthStore.subscribe(
    (state) => state.isAuthenticated,
    (isAuthenticated) => {
        console.log("isAuthenticated changed to:", isAuthenticated);
    }
);

Testing

Reset dello Store nei Test

typescript
import { useAuthStore } from "@/store/auth/auth.store";

beforeEach(() => {
    // Reset completo dello store
    useAuthStore.getState().logout();
});

Test di Login

typescript
import { useAuthStore } from "@/store/auth/auth.store";

it("should login user", () => {
    // Arrange
    useAuthStore.getState().logout();

    // Act
    useAuthStore.getState().login({ redirect: "main", module: "dashboard" });

    // Assert
    expect(useAuthStore.getState().isAuthenticated).toBe(true);
    expect(useAuthStore.getState().loginData?.redirect).toBe("main");
});

Test di Componenti

typescript
import { render, screen } from "@testing-library/react";
import { useAuthStore } from "@/store/auth/auth.store";

it("should show user info when authenticated", () => {
    // Setup store state
    useAuthStore.getState().login({ redirect: "main" });
    useAuthStore.getState().setUserSettings({ user: { name: "Mario" } });

    // Render component
    render(<UserInfo />);

    // Assert
    expect(screen.getByText(/Mario/)).toBeInTheDocument();
});

Best Practices

✅ DO

typescript
// 1. Usa selectors per performance
const isAuth = useAuthStore((state) => state.isAuthenticated);

// 2. Accedi alle actions con selector
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);

// 5. Resetta lo store nei test
beforeEach(() => useAuthStore.getState().logout());

❌ DON'T

typescript
// 1. Non destrutturare l'intero store
const { isAuth, login, logout, loginData } = useAuthStore(); // ❌

// 2. Non chiamare useAuthStore() fuori da React
function myFunction() {
    const store = useAuthStore(); // ❌ Errore hooks!
    // Usa invece: useAuthStore.getState()
}

// 3. Non creare selectors che ritornano tutto
useAuthStore((state) => state); // ❌ Re-render ad ogni cambio

// 4. Non modificare lo stato direttamente
useAuthStore.getState().isAuthenticated = true; // ❌
// Usa invece: useAuthStore.getState().login(data)

Struttura dello Store

typescript
// app/store/auth/auth.store.ts
import { create } from "zustand";
import { persist, createJSONStorage } from "zustand/middleware";

interface AuthState {
    // Stato
    isAuthenticated: boolean;
    loginData: LoginData | null;
    userSettings: UserSettings | null;

    // Actions
    login: (data: LoginData) => void;
    logout: () => void;
    setLoginData: (data: LoginData | null) => void;
    setUserSettings: (settings: UserSettings | null) => void;
}

export const useAuthStore = create<AuthState>()(
    persist(
        (set) => ({
            isAuthenticated: false,
            loginData: null,
            userSettings: null,

            login: (data) => set({ isAuthenticated: true, loginData: data }),
            logout: () => set({
                isAuthenticated: false,
                loginData: null,
                userSettings: null
            }),
            setLoginData: (data) => set({ loginData: data }),
            setUserSettings: (settings) => set({ userSettings: settings }),
        }),
        {
            name: "auth-storage",
            storage: createJSONStorage(() => localStorage),
        }
    )
);

Riferimenti

Documentazione Elerama Frontend