21 aprile 2026

Zustand in React: stato globale semplice, selettori e performance (senza impazzire con Context)

Dallo store “hook-based” ai selector: come evitare rerender inutili, organizzare le azioni e usare lo stato anche fuori dai componenti.

Zustand è una libreria di state management minimalista ma sorprendentemente potente. In questo articolo vediamo come passare da un approccio basato su React Context a uno store Zustand, perché migliora le performance (se usato nel modo giusto) e quali pattern conviene adottare per azioni e selettori, inclusa la shallow comparison per evitare rerender indesiderati.

Perché guardare oltre React Context

React Context è una soluzione comoda per condividere stato “globale” (o quasi) senza prop-drilling. Il problema emerge quando lo usi come state management general-purpose:

  • Ogni cambiamento nel context può innescare rerender di tutti i componenti che lo consumano, anche se usano solo una piccola parte dei dati.
  • La scalabilità peggiora: più lo stato cresce (e più punti lo consumano), più diventa difficile mantenere performance e chiarezza.

Un caso tipico: un contatore con un componente che visualizza count e un altro con i controlli increment/decrement/reset. Se entrambi consumano lo stesso context, al cambio di count possono rerenderare entrambi… anche se i controlli non dipendono da count.

Zustand risolve questo scenario in modo molto pragmatico: uno store esterno a React con un’API semplice, e un hook che ti permette di sottoscrivere solo ciò che ti serve.


Creare uno store Zustand: la base

Installa la libreria:

npm i zustand

Crea uno store, ad esempio useCounterStore.ts:

import { create } from "zustand"

type CounterState = {
  count: number
  increment: () => void
  decrement: () => void
  reset: () => void
}

export const useCounterStore = create<CounterState>((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
  reset: () => set({ count: 0 }),
}))

Osservazioni importanti:

  • create restituisce un hook (useCounterStore) che userai nei componenti.
  • set funziona come setState in React: può ricevere un oggetto o una funzione.
  • Quando fai set({ count: 0 }), Zustand mergia lo stato: non devi fare ...state manualmente.

Il punto chiave: i selector (o perdi i vantaggi)

Se nei componenti fai:

const store = useCounterStore()

stai sottoscrivendo l’intero store. Risultato: se cambia una qualsiasi parte, il componente rerenderizza.

La pratica corretta è usare un selector:

Display: sottoscrivi solo count

const count = useCounterStore((state) => state.count)

Controls: sottoscrivi solo le funzioni che ti servono

const increment = useCounterStore((state) => state.increment)
const decrement = useCounterStore((state) => state.decrement)
const reset = useCounterStore((state) => state.reset)

Così:

  • Il display rerenderizza quando cambia count.
  • I controlli non rerenderizzano quando cambia count (le funzioni non cambiano reference di solito).

Questo è uno dei motivi principali per cui Zustand è spesso più efficiente di un Context “onnicomprensivo”.


Se vuoi selezionare più cose insieme: attenzione all’oggetto

Potresti voler scrivere:

const { increment, decrement, reset } = useCounterStore((state) => ({
  increment: state.increment,
  decrement: state.decrement,
  reset: state.reset,
}))

Sembra pulito, ma qui c’è un dettaglio: stai creando un nuovo oggetto ad ogni esecuzione del selector. Se il confronto viene fatto sulla reference, il componente può rerenderizzare più del necessario.

Soluzione: useShallow

Zustand mette a disposizione un helper per fare shallow comparison:

import { useShallow } from "zustand/shallow"

const { increment, decrement, reset } = useCounterStore(
  useShallow((state) => ({
    increment: state.increment,
    decrement: state.decrement,
    reset: state.reset,
  }))
)

Con la shallow comparison, invece di confrontare l’oggetto “in blocco”, vengono confrontate le singole proprietà: se le funzioni sono le stesse, niente rerender.


Accesso allo stato fuori dai componenti React

Una differenza enorme rispetto a Context: lo store Zustand vive fuori da React. Quindi puoi leggerlo o aggiornarlo anche in funzioni “normali”, senza dover passare callback tramite props o context.

Esempi:

// leggere lo stato corrente
const current = useCounterStore.getState().count

// eseguire un'azione esistente
useCounterStore.getState().increment()

// impostare stato direttamente
useCounterStore.setState({ count: 10 })

Questo è utilissimo per:

  • listener globali (es. eventi del browser)
  • integrazioni con librerie esterne
  • utility che vivono fuori dall’albero React

Come organizzare le azioni: due pattern comuni

1) Azioni dentro lo store (con namespace actions)

Se ti piace avere “stato + azioni” nello stesso posto, puoi raggruppare:

type CounterState = {
  count: number
  actions: {
    increment: () => void
    decrement: () => void
    reset: () => void
  }
}

export const useCounterStore = create<CounterState>((set) => ({
  count: 0,
  actions: {
    increment: () => set((s) => ({ count: s.count + 1 })),
    decrement: () => set((s) => ({ count: s.count - 1 })),
    reset: () => set({ count: 0 }),
  },
}))

Nei componenti:

const increment = useCounterStore((s) => s.actions.increment)

Pro: struttura chiara. Contro: lo store cresce e diventa un “contenitore di tutto”.

2) Store solo dati, azioni esportate a parte

Se preferisci uno store come “fonte dati” e azioni come funzioni separate:

import { create } from "zustand"

type CounterState = { count: number }

export const useCounterStore = create<CounterState>(() => ({ count: 0 }))

export const increment = () =>
  useCounterStore.setState((s) => ({ count: s.count + 1 }))

export const decrement = () =>
  useCounterStore.setState((s) => ({ count: s.count - 1 }))

export const reset = () =>
  useCounterStore.setState({ count: 0 })

Pro: separazione netta, funzioni riusabili ovunque. Contro: devi gestire naming/import con più disciplina.


Linee guida pratiche

  • Non chiamare useStore() senza selector se lo store non è microscopico.
  • Se devi selezionare più valori e ritorni un oggetto, considera useShallow.
  • Decidi un pattern per le azioni (dentro store vs fuori) e mantienilo coerente nel progetto.
  • Per stato davvero globale e condiviso in tanti punti, Zustand tende a essere più semplice e performante di un Context usato come store.

Conclusione

Zustand dà il meglio quando sfrutti la sua idea centrale: sottoscrivere solo ciò che ti serve tramite selector. Da lì ottieni un flusso di stato globale più pulito, componenti che rerenderizzano meno e la libertà di usare lo stato anche fuori dall’albero React.

Se vuoi, nel prossimo step ha senso parlare di persistenza, middleware e composizione di store: è lì che Zustand diventa davvero “da produzione” senza perdere la sua semplicità.