Best Practice
Quando Usare Zustand vs Context vs Redux
| Criterio | Context API | Zustand | Redux Toolkit |
|---|---|---|---|
| Stato condiviso semplice | Ottimo | Buono | Eccessivo |
| Performance (re-render) | Problematico | Eccellente | Eccellente |
| Boilerplate | Minimo | Minimo | Moderato |
| DevTools | No | Si (via middleware) | Si (nativo) |
| Middleware/Plugin | No | Si | Si |
| Stato fuori da React | No | Si | Si |
| Curva di apprendimento | Bassa | Bassa | Media |
| Dimensione bundle | 0 KB | ~1 KB | ~11 KB |
Usa Context per temi, locale, configurazione statica che cambia raramente. Usa Zustand per stato dell’applicazione che cambia frequentemente e richiede performance. Usa Redux se il team lo conosce gia’ e il progetto ha middleware complessi.
Struttura dello Store
Un Store Globale vs Multi Store
// APPROCCIO 1: Store singolo con slices (consigliato per app medie)
const useAppStore = create<AppStore>()((...args) => ({
...createAuthSlice(...args),
...createCartSlice(...args),
...createUISlice(...args),
}))
// APPROCCIO 2: Store separati (consigliato per app grandi o micro-frontend)
const useAuthStore = create<AuthState>()(/* ... */)
const useCartStore = create<CartState>()(/* ... */)
const useUIStore = create<UIState>()(/* ... */)
Store separati sono piu’ facili da testare e mantenere indipendentemente. Lo store singolo e’ piu’ semplice quando gli slices devono comunicare tra loro.
Granularita’ dello Stato
Mantieni lo store piatto e con valori il piu’ granulari possibile.
// BAD: oggetto annidato rende difficile selezionare
interface BadStore {
form: {
user: { name: string; email: string }
settings: { theme: string; lang: string }
}
}
// GOOD: valori piatti, facili da selezionare
interface GoodStore {
userName: string
userEmail: string
theme: string
lang: string
}
Se il nesting e’ inevitabile (es. lista di oggetti), usa il middleware immer per semplificare gli aggiornamenti.
Azioni Fuori dal Componente
Le azioni possono essere chiamate ovunque, non solo dai componenti React.
// utils/notifications.ts
import { useNotificationStore } from '@/stores/notificationStore'
export function showError(message: string) {
useNotificationStore.getState().addNotification({
type: 'error',
message,
timestamp: Date.now(),
})
}
// Usabile in qualsiasi file
import { showError } from '@/utils/notifications'
async function fetchData() {
try {
const res = await fetch('/api/data')
if (!res.ok) showError('Errore nel caricamento dei dati')
} catch {
showError('Errore di rete')
}
}
Questo pattern mantiene la logica di business separata dai componenti React.
Evitare Stale Closures
Quando usi get() dentro un’azione, ottieni sempre lo stato aggiornato. Ma attenzione alle closures nei componenti.
// BAD: count potrebbe essere stale in un timeout
function BadComponent() {
const count = useStore((s) => s.count)
const handleClick = () => {
setTimeout(() => {
console.log(count) // Potrebbe essere il valore vecchio
}, 3000)
}
}
// GOOD: leggi lo stato al momento dell'esecuzione
function GoodComponent() {
const handleClick = () => {
setTimeout(() => {
const count = useStore.getState().count // Sempre aggiornato
console.log(count)
}, 3000)
}
}
Pattern Comuni
Stato Derivato (Computed Values)
Non memorizzare nello store valori che possono essere calcolati.
// BAD: 'total' e 'count' sono ridondanti
interface BadCartStore {
items: CartItem[]
total: number // ridondante
count: number // ridondante
}
// GOOD: calcola nei selettori
const useCartStore = create<CartStore>()((set) => ({
items: [],
addItem: (item) => set((s) => ({ items: [...s.items, item] })),
}))
// Selettori derivati
const selectTotal = (s: CartStore) =>
s.items.reduce((sum, i) => sum + i.price * i.qty, 0)
const selectCount = (s: CartStore) => s.items.length
function CartSummary() {
const total = useCartStore(selectTotal)
const count = useCartStore(selectCount)
return <p>{count} articoli - Totale: {total.toFixed(2)} EUR</p>
}
Azioni come Funzioni Esterne
Per azioni complesse, puoi definirle fuori dallo store per migliore testabilita’.
// actions/cartActions.ts
import { useCartStore } from '@/stores/cartStore'
export async function checkout() {
const { items } = useCartStore.getState()
const res = await fetch('/api/checkout', {
method: 'POST',
body: JSON.stringify({ items }),
})
if (res.ok) {
useCartStore.setState({ items: [] })
}
}
Reset Pattern
const initialState = {
items: [],
filter: 'all',
search: '',
}
const useStore = create<StoreState>()((set) => ({
...initialState,
// azioni...
reset: () => set(initialState, true), // true = replace
}))
Seguendo queste best practice, il codice resta manutenibile, performante e facile da testare anche in progetti di grandi dimensioni.