import { create } from 'zustand';
type CartState = {
items: string[];
addItem: (item: string) => void;
};
// Store global decuplat de React tree
export const useCartStore = create<CartState>((set) => ({
items: [],
addItem: (item) => set((state) => ({ items: [...state.items, item] })),
}));
// În componentă: re-render DOAR când lungimea se schimbă
export function CartCount() {
const count = useCartStore((s) => s.items.length);
return <span>Produse: {count}</span>;
}Am refăcut anul trecut o bucată critică dintr-un SaaS B2B unde aveam vreo 12k utilizatori activi zilnic. Trecerea de la un păienjeniș de useReducer trimis prin React Context la Zustand ne-a redus codul aferent stării cu aproape 40% și a rezolvat câteva probleme urâte de performanță.
Totuși, asta nu înseamnă că useReducer e învechit sau că ar trebui să arunci Zustand în orice componentă de formular.
Când e useReducer exact ce îți trebuie
Principala calitate a lui useReducer este că izolează starea strict în arborele de componente unde este folosit. Când componenta se demontează, starea dispare automat din memorie fără niciun cleanup manual.
Am avut cazul unui builder de formulare dinamice cu 5 pași și validări încrucișate. Starea era complexă — decizii de branching, câmpuri generate pe loc, istoricul de undo/redo. Totuși, acea stare nu interesa pe nimeni în afara acelui modal.
Folosind useReducer local, am garantat că starea trăiește exact cât trăiește modalul. Nu am avut riscul să rămână date agățate în memorie când userul închidea fereastra, iar testarea reducer-ului a fost banală, fiind o funcție pură JS făra nicio dependență de vreo librărie terță.
Unde crapă combinația useReducer + Context API
Problema apare când vrei să împarți starea din useReducer cu componente aflate adânc în arbore. Soluția nativă e să o pui într-un React Context, dar aici ne-am furat-o de câteva ori.
În primul rând, apare celebrul provider hell. Ajunsesem să avem 7 sau 8 Provideri înveliți unul în altul la nivel de pagină. În al doilea rând, performanța: orice schimbare în obiectul de stare din Context va declanșa re-randarea tuturor componentelor consumatoare, chiar dacă ele folosesc doar un singur câmp care nu s-a schimbat.
Există hack-uri cu mai multe contexte separate (unul pentru stare, unul pentru dispatch), dar boilerplate-ul devine obositor rapid.
De ce Zustand e adesea o alegere mai sănătoasă
Zustand rezolvă eleganța accesului global și a performanței prin două chestii simple: subscripții bazate pe selectori și decuplarea totală de arborele de React.
Când folosești un selector precum useStore(state => state.isSidebarOpen), componenta aia se va re-randa doar când se schimbă booleanul respectiv. Nu tot obiectul, nu restul stării.
Iar cel mai mare plus pe care l-am simțit la un proiect mai vechi: poți accesa sau modifica starea din Zustand direct din afara React — de exemplu, într-un interceptor de Axios când primești un 401 și vrei să apelezi useAuthStore.getState().logout().
Trade-off-ul invizibil din Zustand
Liniștea pe care ți-o dă Zustand vine cu o capcană. Pentru că e atât de ușor să creezi un store și să-l imporți oriunde, e foarte tentant să arunci toată starea aplicației acolo.
Am văzut echipe care puneau în Zustand inclusiv starea de isOpen a unui dropdown local. Rezultatul? A dispărut încapsularea. Componente care trebuiau să fie reutilizabile au ajuns strâns legate de un store global specific, făcând refactoring-ul un chin.
Regula mea simplă: dacă starea trebuie să moară odată cu componenta sau ecranul respectiv, rămâne locală (useState sau useReducer). Dacă starea trebuie să supraviețuiască navigării sau e accesată din 3 locuri complet izolate din app, merge în Zustand.
Voi unde trageți linia între starea locală și cea globală?