Gestionarea stării în React Native e una din chestiile care te pot frustra serios. Dacă ai lucrat cu Redux — și probabil ai lucrat, toți am trecut pe acolo — știi exact câte fișiere, câte action types și câți reduceri sunt necesari pentru a face ceva aparent simplu. Ei bine, în 2026, Zustand a schimbat complet jocul. A devenit practic alegerea implicită pentru majoritatea dezvoltatorilor React Native, și sincer, e ușor de înțeles de ce.
În acest ghid vei învăța tot ce trebuie despre Zustand v5: de la un store simplu până la pattern-uri avansate — middleware persist cu MMKV, slices pattern pentru aplicații mai complexe și integrarea cu Expo. Am pus exemple de cod funcționale în fiecare secțiune, deci poți copia direct în proiectul tău fără surprize.
Ce este Zustand și de ce domină în 2026
Zustand (cuvânt german pentru „stare" — da, nemții au cuvânt pentru orice) este o bibliotecă minimalistă de state management creată de echipa Poimandres (pmndrs). Cu doar ~1KB comprimat, oferă un API pe bază de hook-uri care elimină complet nevoia de Provider-i, reduceri sau action types.
Cam 40% din proiectele noi React Native în 2026 folosesc Zustand. Redux Toolkit încă are locul lui în aplicațiile enterprise, dar pentru tot restul — de la startup-uri la aplicații de dimensiuni medii — Zustand e alegerea implicită.
Avantajele cheie ale Zustand
- Zero boilerplate — nu ai nevoie de action creators, reduceri sau dispatch
- Fără Provider — nu trebuie să împachetezi aplicația într-un wrapper special
- Selectori inteligenți — componentele se re-randează doar când se schimbă datele la care sunt abonate
- Acces în afara React — poți citi și modifica starea de oriunde prin
getState()șisetState() - Suport nativ TypeScript — tipizare completă, fără configurări extra
- Middleware extensibil — persist, immer, devtools și subscribeWithSelector
Instalare și configurare în Expo / React Native
Zustand funcționează fără probleme atât cu Expo (managed și bare workflow) cât și cu React Native CLI. Instalarea e simplă:
# Cu npm
npm install zustand
# Cu yarn
yarn add zustand
# Cu pnpm
pnpm add zustand
Zustand v5 cere React 18 sau mai nou, dar asta nu e o problemă în 2026 — Expo SDK 55 și React Native 0.77+ vin deja cu React 18.3+.
Și asta e tot. Serios. Nu mai trebuie să configurezi nimic altceva — niciun store separat, niciun Provider, niciun wrapper în jurul componentei rădăcină. Spre deosebire de Redux, poți pur și simplu să începi să scrii cod.
Crearea primului store Zustand cu TypeScript
Hai să construim un store pentru un contor simplu ca să vezi cât de direct e totul:
// stores/counterStore.ts
import { create } from 'zustand';
interface CounterState {
count: number;
increase: () => void;
decrease: () => void;
reset: () => void;
}
export const useCounterStore = create<CounterState>()((set) => ({
count: 0,
increase: () => set((state) => ({ count: state.count + 1 })),
decrease: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
}));
Un detaliu important dacă folosești TypeScript: vezi sintaxa create<CounterState>()() cu paranteze duble? E necesară în Zustand v5 când combini TypeScript cu middleware. La prima vedere pare ciudat, dar te obișnuiești rapid.
Utilizarea store-ului într-o componentă React Native
// screens/CounterScreen.tsx
import React from 'react';
import { View, Text, Button, StyleSheet } from 'react-native';
import { useCounterStore } from '../stores/counterStore';
export default function CounterScreen() {
const count = useCounterStore((state) => state.count);
const increase = useCounterStore((state) => state.increase);
const decrease = useCounterStore((state) => state.decrease);
const reset = useCounterStore((state) => state.reset);
return (
<View style={styles.container}>
<Text style={styles.counter}>{count}</Text>
<Button title="Crește" onPress={increase} />
<Button title="Scade" onPress={decrease} />
<Button title="Resetează" onPress={reset} />
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, justifyContent: 'center', alignItems: 'center' },
counter: { fontSize: 48, fontWeight: 'bold', marginBottom: 20 },
});
Fiecare apel useCounterStore((state) => state.count) este un selector. Componenta se re-randează exclusiv când valoarea count se schimbă — nu la orice altă modificare din store. Simplu și eficient.
Selectori și prevenirea re-randărilor inutile
Re-randările inutile sunt dușmanul numărul 1 al performanței în React Native. FPS-ul scade, animațiile se blochează, utilizatorii se plâng. Zustand rezolvă treaba asta elegant prin selectori granulari.
Regula de aur: un selector per proprietate
// ✅ Corect — componenta se re-randează doar când 'count' se schimbă
const count = useCounterStore((state) => state.count);
// ❌ Greșit — componenta se re-randează la ORICE schimbare din store
const store = useCounterStore();
useShallow pentru selectori multipli (obligatoriu în v5)
Dacă ai nevoie de mai multe valori simultan, folosește useShallow din zustand/shallow. Atenție mare aici — fără acest hook, selectorii care returnează obiecte noi vor cauza probleme serioase în Zustand v5:
import { useShallow } from 'zustand/shallow';
// ✅ Corect cu useShallow
const { count, increase } = useCounterStore(
useShallow((state) => ({ count: state.count, increase: state.increase }))
);
// ❌ PERICOL — buclă infinită de re-randări în v5!
const { count, increase } = useCounterStore(
(state) => ({ count: state.count, increase: state.increase })
);
Asta e probabil cel mai important lucru de reținut din tot articolul: un selector care returnează un obiect nou fără useShallow nu cauzează doar niște re-randări în plus — provoacă o eroare Maximum update depth exceeded care îți demontează întregul arbore de componente. Am pățit-o și eu, și nu e plăcut să depanezi.
Operații asincrone: fetch, API-uri și încărcare de date
Aici Zustand chiar strălucește față de Redux. Nu ai nevoie de niciun middleware extern — niciun Thunk, niciun Saga. Pur și simplu scrii funcții async direct în store:
// stores/userStore.ts
import { create } from 'zustand';
interface User {
id: number;
name: string;
email: string;
}
interface UserState {
users: User[];
loading: boolean;
error: string | null;
fetchUsers: () => Promise<void>;
}
export const useUserStore = create<UserState>()((set) => ({
users: [],
loading: false,
error: null,
fetchUsers: async () => {
set({ loading: true, error: null });
try {
const response = await fetch('https://api.example.com/users');
const data: User[] = await response.json();
set({ users: data, loading: false });
} catch (err) {
set({ error: 'Eroare la încărcarea utilizatorilor', loading: false });
}
},
}));
Totuși, un sfat important: dacă datele vin de la un API (deci stare de server), e mai bine să folosești TanStack Query (React Query) în loc de Zustand. Zustand e perfect pentru starea clientului — preferințe utilizator, temă, autentificare, coș de cumpărături — dar nu e gândit ca un cache de server.
Middleware: persist, immer și devtools
Middleware-urile sunt cele care transformă Zustand dintr-o jucărie simplă într-o soluție serioasă de producție. Hai să le luăm pe rând.
Persist — salvarea stării între sesiuni
Middleware-ul persist salvează automat starea și o restaurează la relansarea aplicației. Pentru React Native, poți folosi AsyncStorage sau (mult mai bine) MMKV:
// stores/settingsStore.ts
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import AsyncStorage from '@react-native-async-storage/async-storage';
interface SettingsState {
theme: 'light' | 'dark';
language: string;
setTheme: (theme: 'light' | 'dark') => void;
setLanguage: (lang: string) => void;
}
export const useSettingsStore = create<SettingsState>()(
persist(
(set) => ({
theme: 'light',
language: 'ro',
setTheme: (theme) => set({ theme }),
setLanguage: (language) => set({ language }),
}),
{
name: 'app-settings',
storage: createJSONStorage(() => AsyncStorage),
}
)
);
Persist cu MMKV — de 30x mai rapid decât AsyncStorage
react-native-mmkv e cea mai rapidă soluție de stocare key/value pentru React Native. Vorbim de aproximativ 30 de ori mai rapid decât AsyncStorage. Totul e sincron — fără async/await, fără Promise-uri, fără overhead de Bridge. Diferența se simte.
# Instalare MMKV
npx expo install react-native-mmkv react-native-nitro-modules
npx expo prebuild
// storage/mmkvStorage.ts
import { MMKV } from 'react-native-mmkv';
import { StateStorage } from 'zustand/middleware';
const mmkv = new MMKV({ id: 'app-storage' });
export const mmkvStorage: StateStorage = {
setItem: (name, value) => mmkv.set(name, value),
getItem: (name) => mmkv.getString(name) ?? null,
removeItem: (name) => mmkv.delete(name),
};
// stores/authStore.ts
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import { mmkvStorage } from '../storage/mmkvStorage';
interface AuthState {
token: string | null;
isAuthenticated: boolean;
login: (token: string) => void;
logout: () => void;
}
export const useAuthStore = create<AuthState>()(
persist(
(set) => ({
token: null,
isAuthenticated: false,
login: (token) => set({ token, isAuthenticated: true }),
logout: () => set({ token: null, isAuthenticated: false }),
}),
{
name: 'auth-storage',
storage: createJSONStorage(() => mmkvStorage),
}
)
);
Immer — actualizări mutabile ale stării
Middleware-ul immer îți permite să scrii actualizări de stare ca și cum ai modifica direct obiectul, dar sub capotă totul rămâne imutabil. E deosebit de util când ai stare profund imbricată (și crede-mă, orice coș de cumpărături devine complicat destul de repede):
import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';
interface CartItem {
id: string;
name: string;
quantity: number;
price: number;
}
interface CartState {
items: CartItem[];
addItem: (item: Omit<CartItem, 'quantity'>) => void;
updateQuantity: (id: string, quantity: number) => void;
removeItem: (id: string) => void;
}
export const useCartStore = create<CartState>()(
immer((set) => ({
items: [],
addItem: (item) =>
set((state) => {
const existing = state.items.find((i) => i.id === item.id);
if (existing) {
existing.quantity += 1;
} else {
state.items.push({ ...item, quantity: 1 });
}
}),
updateQuantity: (id, quantity) =>
set((state) => {
const item = state.items.find((i) => i.id === id);
if (item) item.quantity = quantity;
}),
removeItem: (id) =>
set((state) => {
state.items = state.items.filter((i) => i.id !== id);
}),
}))
);
Observă cum poți scrie direct state.items.push() sau existing.quantity += 1 — immer se ocupă de imutabilitate în spatele scenei.
DevTools — debugging cu Redux DevTools
Da, ai citit bine — poți folosi Redux DevTools cu Zustand. Middleware-ul devtools îți oferă vizibilitate completă asupra schimbărilor de stare:
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
export const useAppStore = create<AppState>()(
devtools(
(set) => ({
// ... starea și acțiunile tale
}),
{ name: 'AppStore' }
)
);
Combinarea middleware-urilor
Poți combina mai multe middleware-uri prin înlănțuire. Ordinea recomandată e devtools → persist → immer (de la exterior spre interior):
import { create } from 'zustand';
import { devtools, persist, createJSONStorage } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
import { mmkvStorage } from '../storage/mmkvStorage';
export const useStore = create<StoreState>()(
devtools(
persist(
immer((set) => ({
// starea ta aici
})),
{
name: 'my-store',
storage: createJSONStorage(() => mmkvStorage),
}
),
{ name: 'MyStore' }
)
);
Slices Pattern: organizarea store-urilor pentru aplicații mari
La un moment dat, un singur store monolitic devine un haos greu de întreținut. Am fost acolo. Când fișierul depășește 200 de linii și fiecare modificare riscă să strice altceva, e timpul să treci la slices pattern.
Ideea e simplă: împarți starea în unități modulare, fiecare gestionând un domeniu specific.
// stores/slices/userSlice.ts
import { StateCreator } from 'zustand';
export interface UserSlice {
user: { name: string; email: string } | null;
setUser: (user: { name: string; email: string }) => void;
clearUser: () => void;
}
export const createUserSlice: StateCreator<
UserSlice & ThemeSlice,
[],
[],
UserSlice
> = (set) => ({
user: null,
setUser: (user) => set({ user }),
clearUser: () => set({ user: null }),
});
// stores/slices/themeSlice.ts
import { StateCreator } from 'zustand';
export interface ThemeSlice {
theme: 'light' | 'dark';
toggleTheme: () => void;
}
export const createThemeSlice: StateCreator<
UserSlice & ThemeSlice,
[],
[],
ThemeSlice
> = (set) => ({
theme: 'light',
toggleTheme: () =>
set((state) => ({ theme: state.theme === 'light' ? 'dark' : 'light' })),
});
// stores/useAppStore.ts
import { create } from 'zustand';
import { createUserSlice, UserSlice } from './slices/userSlice';
import { createThemeSlice, ThemeSlice } from './slices/themeSlice';
type AppStore = UserSlice & ThemeSlice;
export const useAppStore = create<AppStore>()((...args) => ({
...createUserSlice(...args),
...createThemeSlice(...args),
}));
Pattern-ul ăsta scalează excelent. Fiecare slice e un fișier separat, testabil independent, și poți adăuga slice-uri noi fără să atingi codul existent.
Acces în afara componentelor React
Una dintre funcționalitățile mele preferate la Zustand (și sincer, motivul principal pentru care l-am ales inițial) e posibilitatea de a accesa starea din afara arborelui React. În React Native, asta e extrem de util pentru:
- Interceptoare Axios
- Servicii de navigare
- Funcții utilitare
- Gestionarea notificărilor push
// utils/apiClient.ts
import axios from 'axios';
import { useAuthStore } from '../stores/authStore';
const apiClient = axios.create({
baseURL: 'https://api.example.com',
});
apiClient.interceptors.request.use((config) => {
// Accesează token-ul direct, fără hook
const token = useAuthStore.getState().token;
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
apiClient.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
// Deconectează utilizatorul automat
useAuthStore.getState().logout();
}
return Promise.reject(error);
}
);
export default apiClient;
Zustand vs Redux Toolkit: comparație practică
Deci, dacă vii din lumea Redux, iată o comparație directă. Cifrele vorbesc de la sine.
Același funcționalitate — contor simplu
Cu Redux Toolkit (~25 linii de configurare):
// Redux: slice + store + provider
import { configureStore, createSlice } from '@reduxjs/toolkit';
import { Provider, useSelector, useDispatch } from 'react-redux';
const counterSlice = createSlice({
name: 'counter',
initialState: { count: 0 },
reducers: {
increment: (state) => { state.count += 1; },
decrement: (state) => { state.count -= 1; },
},
});
const store = configureStore({ reducer: { counter: counterSlice.reducer } });
// + Provider wrapper în App.tsx
// + useSelector/useDispatch în componente
Cu Zustand (~10 linii, totul inclus):
// Zustand: un singur fișier, niciun Provider
import { create } from 'zustand';
const useCounterStore = create((set) => ({
count: 0,
increment: () => set((s) => ({ count: s.count + 1 })),
decrement: () => set((s) => ({ count: s.count - 1 })),
}));
// Folosire directă: const count = useCounterStore(s => s.count);
Diferența nu e doar de linii de cod — e de complexitate mentală. Cu Zustand, tot ce trebuie să înțelegi e create, set și selectorii. Cam atât.
Când să alegi fiecare soluție
| Criteriu | Zustand | Redux Toolkit |
|---|---|---|
| Dimensiune aplicație | Mică → Mare | Mare → Enterprise |
| Dimensiune bundle | ~1KB | ~10KB+ |
| Boilerplate | Minim | Moderat (redus cu RTK) |
| Curba de învățare | Foarte ușoară | Moderată |
| DevTools | Da (middleware) | Da (integrat) |
| Echipă mare (5+ dev) | Funcționează bine cu convenții | Structură rigidă avantajoasă |
| Middleware avansat | persist, immer, devtools | Thunk, Saga, RTK Query |
Hidratarea stării și prevenirea flash-ului inițial
Când folosești persist, există un moment scurt între lansarea aplicației și restaurarea stării salvate. În acel interval, componentele afișează valorile inițiale — și asta poate cauza un „flash" vizibil destul de neplăcut (de exemplu, tema light apare o fracțiune de secundă înainte de tema dark salvată).
Soluția: verifică dacă hidratarea s-a terminat înainte de a afișa conținutul.
// components/HydrationGate.tsx
import React, { useEffect, useState } from 'react';
import { ActivityIndicator, View } from 'react-native';
import { useSettingsStore } from '../stores/settingsStore';
export function HydrationGate({ children }: { children: React.ReactNode }) {
const [hydrated, setHydrated] = useState(false);
useEffect(() => {
const unsub = useSettingsStore.persist.onFinishHydration(() => {
setHydrated(true);
});
// Verifică dacă hidratarea s-a terminat deja
if (useSettingsStore.persist.hasHydrated()) {
setHydrated(true);
}
return unsub;
}, []);
if (!hydrated) {
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<ActivityIndicator size="large" />
</View>
);
}
return <>{children}</>;
}
Structura recomandată a proiectului
Dacă te întrebi cum să organizezi totul, iată o structură care funcționează bine într-o aplicație Expo cu Zustand:
src/
├── app/ # Expo Router - rute și layout-uri
│ ├── _layout.tsx
│ ├── index.tsx
│ └── (tabs)/
├── stores/ # Toate store-urile Zustand
│ ├── authStore.ts
│ ├── settingsStore.ts
│ ├── cartStore.ts
│ └── slices/ # Pentru aplicații mari — slices pattern
│ ├── userSlice.ts
│ └── themeSlice.ts
├── storage/ # Configurare stocare (MMKV / AsyncStorage)
│ └── mmkvStorage.ts
├── components/
│ └── HydrationGate.tsx
└── utils/
└── apiClient.ts
Greșeli frecvente și cum să le eviți
Am adunat aici cele mai frecvente capcane pe care le întâlnesc dezvoltatorii React Native cu Zustand. Dacă eviți astea, ești deja cu un pas înainte.
1. Selectori care returnează obiecte noi fără useShallow
Am menționat deja asta, dar merită repetat — e greșeala numărul 1 în Zustand v5. Un selector ca (state) => ({ a: state.a, b: state.b }) creează un obiect nou la fiecare randare și declanșează o buclă infinită. Folosește mereu useShallow pentru selectori care returnează obiecte sau array-uri.
2. Stocarea datelor de la server în Zustand
Nu folosi Zustand ca cache pentru date de la API. E tentant, dar TanStack Query gestionează mult mai bine invalidarea, refetch-ul și stale data. Zustand rămâne pentru starea clientului.
3. Un store gigantic pentru totul
Dacă store-ul tău depășește vreo 100 de linii, e semn clar că trebuie să treci la slices pattern sau să creezi store-uri separate. Nu amâna — devine din ce în ce mai greu cu timpul.
4. Apelarea întregului store fără selectori
useStore() fără selector = componenta se re-randează la orice schimbare din store. Practic anulezi tot avantajul Zustand.
Întrebări frecvente (FAQ)
Este Zustand potrivit pentru aplicații React Native de producție în 2026?
Absolut. Zustand e folosit în producție de companii precum Shopify. Cu versiunea 5.0, ai suport complet pentru React 18, concurrent mode și noua arhitectură React Native (Fabric, Hermes). Dimensiunea mică (~1KB) și lipsa Provider-ului îl fac ideal pentru aplicații mobile unde fiecare kilobyte contează.
Pot folosi Zustand cu Expo Go sau am nevoie de development build?
Zustand merge perfect cu Expo Go, fără configurări speciale. Dacă vrei MMKV pentru persistență rapidă, atunci da, vei avea nevoie de un development build (npx expo prebuild) fiindcă MMKV necesită cod nativ. Ca alternativă, AsyncStorage funcționează cu Expo Go fără prebuild.
Zustand înlocuiește complet Redux în 2026?
Pentru cam 90% din cazuri, da. Dar Redux Toolkit încă are sens pentru aplicații enterprise foarte mari (cu 5+ dezvoltatori), unde structura strictă și middleware-ul avansat (Redux Saga, RTK Query) justifică complexitatea suplimentară.
Cum gestionez starea de autentificare cu Zustand în React Native?
Creează un store dedicat cu middleware-ul persist combinat cu MMKV. Stochezi token-ul JWT, starea de autentificare și acțiunile de login/logout. Apoi folosești getState() în interceptoarele Axios pentru a trimite token-ul automat cu fiecare cerere și a gestiona expirarea sesiunii.
Zustand funcționează cu React Native New Architecture (Fabric, TurboModules)?
Da, fără probleme. Zustand v5 folosește nativ useSyncExternalStore, ceea ce îl face complet compatibil cu Fabric renderer și concurrent rendering. Nu e nevoie de configurări speciale — funcționează direct cu React Native 0.77+ și Expo SDK 55.