Správa stavu je páteří každé mobilní aplikace — a upřímně, bez ní se věci rychle rozpadají. Komponenty nevědí, co zobrazit, formuláře ztrácejí data a celá aplikace se chová... no, nepředvídatelně. V ekosystému React Native jsme roky řešili stejné dilema: použít React Context, který je jednoduchý, ale pomalý? Nebo Redux, který je mocný, ale zahlcený boilerplate kódem? V roce 2026 existuje třetí cesta. A jmenuje se Zustand.
Zustand (německy „stav") je minimalistická knihovna pro správu stavu, která si za posledních pár let získala obrovskou popularitu. Pod jeden kilobajt po kompresi, hook-based API, nulová potřeba providerů. Pro React Native aplikace — od malých prototypů po produkční projekty s miliony uživatelů — je to zkrátka ideální volba.
V tomto průvodci projdeme úplně vším: instalace, vytváření storů, TypeScript integrace, asynchronní akce, persistentní stav a pokročilé optimalizační techniky. Všechno s funkčními příklady kódu, které si můžete rovnou zkopírovat do projektu.
Proč Zustand a ne Redux nebo Context API?
Než se pustíme do kódu, pojďme si ujasnit, proč Zustand v roce 2026 vítězí nad alternativami. Nejde jen o módní trend — za tím stojí konkrétní technické důvody.
Problém s Context API
React Context API je skvělý pro předávání statických hodnot — téma, jazyk, konfigurace. Jako globální state management má ale zásadní slabinu: každá změna kontextu způsobí překreslení všech komponent, které tento kontext konzumují.
U malé aplikace to nevadí. U aplikace s desítkami obrazovek a stovkami komponent? To už uživatel pocítí v podobě pomalého UI.
Problém s Reduxem
Redux Toolkit (RTK) tyhle problémy řeší, ale za cenu značné komplexity. Musíte definovat slice, akce, reducery, nastavit store s middleware a obalit celou aplikaci v Provideru. Pro velký enterprise tým se stovkou vývojářů to dává smysl — striktní pravidla udržují pořádek. Ale pro většinu projektů? Zbytečná režie.
Zustand: zlatá střední cesta
Zustand nabízí to nejlepší z obou přístupů:
- Žádné providery — nepotřebujete obalovat aplikaci v
<Provider>, store je obyčejný hook - Minimální boilerplate — stav a akce definujete na jednom místě, žádné akční typy ani reducery
- Selektivní překreslování — komponenty se překreslí jen tehdy, když se změní ta konkrétní část stavu, kterou odebírají
- Velikost pod 1 KB — oproti ~15 KB pro Redux Toolkit + react-redux
- Plná podpora TypeScriptu — typová inference funguje z krabice
- Kompatibilita s New Architecture — Zustand v5 používá nativní
useSyncExternalStore, takže bezproblémově funguje s Fabric a concurrent renderingem
Instalace a nastavení v Expo / React Native projektu
Začneme instalací. Zustand nemá žádné peer dependence (kromě samotného Reactu), takže je to opravdu triviální.
Nový Expo projekt
# Vytvoření nového projektu
npx create-expo-app@latest moje-aplikace
cd moje-aplikace
# Instalace Zustand
npx expo install zustand
Existující React Native projekt
# npm
npm install zustand
# yarn
yarn add zustand
# pnpm
pnpm add zustand
A to je vše. Žádná konfigurace, žádné providery, žádný setup. Rovnou můžete začít Zustand používat ve svých komponentách. Vážně, nic víc není potřeba.
Vytvoření prvního storu
Store v Zustand je vlastně jenom hook. Vytvoříte ho pomocí funkce create a výsledkem je hook, který můžete volat v jakékoli komponentě. Pojďme si to ukázat na praktickém příkladu — nákupní košík, protože ten řeší každý.
// stores/useCartStore.ts
import { create } from 'zustand';
interface CartItem {
id: string;
name: string;
price: number;
quantity: number;
}
interface CartState {
items: CartItem[];
addItem: (item: Omit<CartItem, 'quantity'>) => void;
removeItem: (id: string) => void;
updateQuantity: (id: string, quantity: number) => void;
clearCart: () => void;
getTotalPrice: () => number;
}
export const useCartStore = create<CartState>()((set, get) => ({
items: [],
addItem: (item) =>
set((state) => {
const existing = state.items.find((i) => i.id === item.id);
if (existing) {
return {
items: state.items.map((i) =>
i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
),
};
}
return { items: [...state.items, { ...item, quantity: 1 }] };
}),
removeItem: (id) =>
set((state) => ({
items: state.items.filter((i) => i.id !== id),
})),
updateQuantity: (id, quantity) =>
set((state) => ({
items: state.items.map((i) =>
i.id === id ? { ...i, quantity: Math.max(0, quantity) } : i
),
})),
clearCart: () => set({ items: [] }),
getTotalPrice: () =>
get().items.reduce((sum, item) => sum + item.price * item.quantity, 0),
}));
Všimněte si syntaxe create<CartState>()() — ty dvojité závorky nejsou překlep. Je to pattern, který Zustand v5 vyžaduje pro správnou typovou inferenci v TypeScriptu. Zpočátku to vypadá divně, ale zvyknete si.
Použití storu v komponentách
Teď ten store využijeme v React Native komponentách. Klíčové je vybírat si ze storu jenom to, co komponenta skutečně potřebuje — tím zajistíte, že se překreslí jen tehdy, když se změní relevantní data.
Komponenta produktu
// components/ProductCard.tsx
import React from 'react';
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
import { useCartStore } from '../stores/useCartStore';
interface ProductCardProps {
id: string;
name: string;
price: number;
}
export function ProductCard({ id, name, price }: ProductCardProps) {
// Vybereme pouze funkci addItem — komponenta se nepřekreslí
// při změně počtu položek v košíku
const addItem = useCartStore((state) => state.addItem);
return (
<View style={styles.card}>
<Text style={styles.name}>{name}</Text>
<Text style={styles.price}>{price} Kč</Text>
<TouchableOpacity
style={styles.button}
onPress={() => addItem({ id, name, price })}
>
<Text style={styles.buttonText}>Přidat do košíku</Text>
</TouchableOpacity>
</View>
);
}
const styles = StyleSheet.create({
card: { padding: 16, backgroundColor: '#1a1a2e', borderRadius: 12, marginBottom: 12 },
name: { fontSize: 18, fontWeight: '600', color: '#ffffff' },
price: { fontSize: 16, color: '#a0a0b0', marginTop: 4 },
button: { marginTop: 12, backgroundColor: '#6c63ff', padding: 12, borderRadius: 8, alignItems: 'center' },
buttonText: { color: '#ffffff', fontWeight: '600' },
});
Komponenta košíku se shrnutím
// components/CartSummary.tsx
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { useCartStore } from '../stores/useCartStore';
export function CartSummary() {
const items = useCartStore((state) => state.items);
const getTotalPrice = useCartStore((state) => state.getTotalPrice);
const totalItems = items.reduce((sum, item) => sum + item.quantity, 0);
return (
<View style={styles.container}>
<Text style={styles.text}>
Položek v košíku: {totalItems}
</Text>
<Text style={styles.total}>
Celkem: {getTotalPrice()} Kč
</Text>
</View>
);
}
const styles = StyleSheet.create({
container: { padding: 16, backgroundColor: '#16213e', borderRadius: 12 },
text: { color: '#a0a0b0', fontSize: 14 },
total: { color: '#ffffff', fontSize: 20, fontWeight: 'bold', marginTop: 4 },
});
Selektory a useShallow: prevence zbytečných překreslení
Tohle je jedna z věcí, na kterou hodně lidí zapomíná, a pak se diví, proč jim aplikace „laguje". Když ze storu vybíráte primitivní hodnotu (string, number, boolean), Zustand automaticky porovnává starou a novou hodnotu pomocí Object.is. Problém ale nastává, když selektor vrací objekt nebo pole — nová reference se vytvoří při každém volání, i když se data nezměnila.
Problém: zbytečné překreslení
// ŠPATNĚ — vytváří nový objekt při každém renderingu
const { theme, language } = useSettingsStore((state) => ({
theme: state.theme,
language: state.language,
}));
// Toto způsobí nekonečnou smyčku v Zustand v5!
Řešení: useShallow
import { useShallow } from 'zustand/react/shallow';
// SPRÁVNĚ — useShallow zajistí povrchové porovnání
const { theme, language } = useSettingsStore(
useShallow((state) => ({
theme: state.theme,
language: state.language,
}))
);
Pravidlo je vlastně docela jednoduché:
- Vybíráte jednu primitivní hodnotu? Stačí prostý selektor.
- Vybíráte objekt nebo pole? Obalte selektor do
useShallow. - Máte hluboce vnořená data, kde shallow porovnání nestačí? V tom případě si napište vlastní deep porovnávací funkci (ale tohle je potřeba jen zřídka).
Asynchronní akce: načítání dat z API
Na rozdíl od Reduxu, kde potřebujete middleware jako redux-thunk nebo redux-saga pro asynchronní operace, Zustand zvládne async akce úplně nativně. Prostě napíšete async funkci a po dokončení zavoláte set. Žádná magie.
// stores/useProductStore.ts
import { create } from 'zustand';
interface Product {
id: string;
name: string;
price: number;
imageUrl: string;
}
interface ProductState {
products: Product[];
isLoading: boolean;
error: string | null;
fetchProducts: () => Promise<void>;
}
export const useProductStore = create<ProductState>()((set) => ({
products: [],
isLoading: false,
error: null,
fetchProducts: async () => {
set({ isLoading: true, error: null });
try {
const response = await fetch('https://api.example.com/products');
if (!response.ok) throw new Error('Nepodařilo se načíst produkty');
const data: Product[] = await response.json();
set({ products: data, isLoading: false });
} catch (error) {
set({
error: error instanceof Error ? error.message : 'Neznámá chyba',
isLoading: false,
});
}
},
}));
Použití v komponentě je pak zcela přímočaré:
import React, { useEffect } from 'react';
import { FlatList, ActivityIndicator, Text } from 'react-native';
import { useProductStore } from '../stores/useProductStore';
export function ProductList() {
const products = useProductStore((state) => state.products);
const isLoading = useProductStore((state) => state.isLoading);
const error = useProductStore((state) => state.error);
const fetchProducts = useProductStore((state) => state.fetchProducts);
useEffect(() => {
fetchProducts();
}, [fetchProducts]);
if (isLoading) return <ActivityIndicator size="large" />;
if (error) return <Text>Chyba: {error}</Text>;
return (
<FlatList
data={products}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<ProductCard
id={item.id}
name={item.name}
price={item.price}
/>
)}
/>
);
}
Persistentní stav: ukládání dat mezi sezeními
Jedna z nejčastějších potřeb v mobilních aplikacích — zachovat stav i po restartování. Přihlášení uživatele, nastavení, obsah košíku. Zustand má pro tohle vestavěný persist middleware a funguje to překvapivě dobře.
Varianta 1: AsyncStorage (jednoduché řešení)
# Instalace AsyncStorage
npx expo install @react-native-async-storage/async-storage
// stores/useSettingsStore.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;
notificationsEnabled: boolean;
setTheme: (theme: 'light' | 'dark') => void;
setLanguage: (language: string) => void;
toggleNotifications: () => void;
}
export const useSettingsStore = create<SettingsState>()(
persist(
(set) => ({
theme: 'dark',
language: 'cs',
notificationsEnabled: true,
setTheme: (theme) => set({ theme }),
setLanguage: (language) => set({ language }),
toggleNotifications: () =>
set((state) => ({ notificationsEnabled: !state.notificationsEnabled })),
}),
{
name: 'settings-storage',
storage: createJSONStorage(() => AsyncStorage),
}
)
);
Varianta 2: MMKV (vysoký výkon)
AsyncStorage je asynchronní a u větších dat pomalý. Pokud potřebujete opravdu bleskovou rychlost, sáhněte po react-native-mmkv. Je přibližně 30krát rychlejší, nabízí synchronní API a dokonce i šifrování. Osobně jsem ho nasadil v jednom projektu jako náhradu za AsyncStorage a rozdíl byl patrný okamžitě — hlavně při startu aplikace.
# Instalace MMKV
npx expo install react-native-mmkv
# Pro MMKV je nutný prebuild v Expo
npx expo prebuild
// lib/mmkvStorage.ts
import { MMKV } from 'react-native-mmkv';
import { StateStorage } from 'zustand/middleware';
const mmkv = new MMKV();
export const mmkvStorage: StateStorage = {
getItem: (name) => {
const value = mmkv.getString(name);
return value ?? null;
},
setItem: (name, value) => {
mmkv.set(name, value);
},
removeItem: (name) => {
mmkv.delete(name);
},
};
// stores/useAuthStore.ts
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import { mmkvStorage } from '../lib/mmkvStorage';
interface AuthState {
token: string | null;
user: { id: string; email: string } | null;
isAuthenticated: boolean;
login: (token: string, user: { id: string; email: string }) => void;
logout: () => void;
}
export const useAuthStore = create<AuthState>()(
persist(
(set) => ({
token: null,
user: null,
isAuthenticated: false,
login: (token, user) => set({ token, user, isAuthenticated: true }),
logout: () => set({ token: null, user: null, isAuthenticated: false }),
}),
{
name: 'auth-storage',
storage: createJSONStorage(() => mmkvStorage),
}
)
);
Porovnání AsyncStorage vs. MMKV
| Vlastnost | AsyncStorage | MMKV |
|---|---|---|
| Rychlost | Pomalý (asynchronní) | ~30× rychlejší (synchronní) |
| Šifrování | Ne | Ano |
| Synchronní API | Ne | Ano |
| Expo kompatibilita | Plná | Vyžaduje prebuild |
| Vhodné pro | Jednoduchá nastavení | Autentizace, velké datasety |
Rozdělení storu na slice: modulární architektura
Jak vaše aplikace roste, jeden monolitický store se stává nepřehledným. Znáte to — soubor s 500 řádky, kde se nikdo nevyzná. Zustand tohle řeší elegantně: rozdělíte logiku do samostatných „slice" a pak je spojíte do jednoho storu.
// stores/slices/authSlice.ts
import { StateCreator } from 'zustand';
export interface AuthSlice {
token: string | null;
isAuthenticated: boolean;
login: (token: string) => void;
logout: () => void;
}
export const createAuthSlice: StateCreator<AuthSlice> = (set) => ({
token: null,
isAuthenticated: false,
login: (token) => set({ token, isAuthenticated: true }),
logout: () => set({ token: null, isAuthenticated: false }),
});
// stores/slices/uiSlice.ts
import { StateCreator } from 'zustand';
export interface UiSlice {
theme: 'light' | 'dark';
isMenuOpen: boolean;
toggleTheme: () => void;
setMenuOpen: (open: boolean) => void;
}
export const createUiSlice: StateCreator<UiSlice> = (set) => ({
theme: 'dark',
isMenuOpen: false,
toggleTheme: () =>
set((state) => ({ theme: state.theme === 'dark' ? 'light' : 'dark' })),
setMenuOpen: (open) => set({ isMenuOpen: open }),
});
// stores/useAppStore.ts
import { create } from 'zustand';
import { createAuthSlice, AuthSlice } from './slices/authSlice';
import { createUiSlice, UiSlice } from './slices/uiSlice';
type AppState = AuthSlice & UiSlice;
export const useAppStore = create<AppState>()((...args) => ({
...createAuthSlice(...args),
...createUiSlice(...args),
}));
Tenhle pattern je obzvlášť užitečný ve větších týmech. Každý vývojář pracuje na svém slice nezávisle na ostatních a nikdo si navzájem neleze do kódu.
Middleware devtools: ladění pomocí React DevTools
Pro efektivní ladění (a věřte mi, budete ho potřebovat) můžete do storu přidat middleware devtools. Zpřístupní vám stav a akce v React DevTools nebo Flipperu.
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
export const useCartStore = create<CartState>()(
devtools(
(set, get) => ({
items: [],
addItem: (item) =>
set(
(state) => ({ items: [...state.items, { ...item, quantity: 1 }] }),
undefined,
'cart/addItem' // název akce viditelný v DevTools
),
// ... další akce
}),
{ name: 'CartStore' }
)
);
Middleware jde samozřejmě kombinovat — třeba devtools s persist:
export const useSettingsStore = create<SettingsState>()(
devtools(
persist(
(set) => ({
// ... definice storu
}),
{ name: 'settings-storage', storage: createJSONStorage(() => AsyncStorage) }
),
{ name: 'SettingsStore' }
)
);
Výkonnostní srovnání: Zustand vs. Redux vs. Context
Čísla říkají víc než slova. Následující benchmarky byly provedeny na reálném zařízení (Android, Pixel 7) s 1000 komponentami odebírajícími stav:
| Metrika | Context API | Redux Toolkit | Zustand v5 |
|---|---|---|---|
| Doba překreslení (1 update) | 45 ms | 18 ms | 12 ms |
| Spotřeba paměti | 4,2 MB | 3,2 MB | 2,1 MB |
| Velikost balíčku | 0 KB (vestavěný) | ~15 KB | ~1 KB |
| Počáteční parsování | 0 ms | 34 ms | 8 ms |
Zustand jasně vede ve všech kategoriích kromě jedné — Context API nepřidává nic k velikosti balíčku, protože je součástí Reactu. Ale jakmile potřebujete globální stav pro cokoliv složitějšího než předávání tématu, Zustand je prostě nejefektivnější volba.
Doporučená struktura projektu
Na závěr se podívejme na to, jak organizovat Zustand story ve vašem React Native projektu. Takhle to funguje dobře v praxi:
src/
├── stores/
│ ├── slices/
│ │ ├── authSlice.ts
│ │ ├── cartSlice.ts
│ │ └── uiSlice.ts
│ ├── useAppStore.ts # Kombinovaný store
│ ├── useProductStore.ts # Samostatný doménový store
│ └── useSearchStore.ts # Samostatný doménový store
├── lib/
│ └── mmkvStorage.ts # MMKV adapter pro persistenci
├── components/
│ └── ...
└── app/
└── ...
Pár základních pravidel, která se mi osvědčila:
- Pro menší aplikace stačí jeden store s kombinovanými slice.
- Pro větší aplikace používejte samostatné doménové story (auth, cart, search) — každý se stará o svou oblast.
- Selektory definujte mimo komponenty, abyste měli stabilní reference.
- Kombinujte Zustand pro klientský stav s TanStack Query pro serverový stav — je to osvědčený pattern, který funguje skvěle v produkci.
Často kladené otázky
Potřebuji Zustand, když mám React Context?
Záleží na složitosti aplikace. Context API je ideální pro statická data (téma, lokalizace), ale pro dynamický stav, který se často mění, způsobuje zbytečné překreslování. Zustand tento problém řeší selektivními subskripcemi — komponenta se překreslí jen tehdy, když se změní data, která skutečně odebírá. Takže pokud má vaše aplikace víc než pár obrazovek s globálním stavem, Zustand vám ušetří spoustu problémů s výkonem.
Je Zustand vhodný pro velké produkční aplikace?
Rozhodně ano. Zustand používají desítky tisíc produkčních aplikací včetně těch s miliony uživatelů. Pro velké projekty doporučuju rozdělit stav do slice, používat useShallow pro selektory vracející objekty a kombinovat Zustand s TanStack Query pro správu serverového stavu. Pokud ale potřebujete vyloženě striktní architekturu pro tým stovky vývojářů, Redux Toolkit může být lepší volba díky vynuceným konvencím.
Jak se Zustand chová s New Architecture v React Native?
Zustand v5 je plně kompatibilní s New Architecture (Fabric, TurboModules, concurrent rendering). Interně používá nativní React API useSyncExternalStore, takže správně funguje i při suspendování, streamování nebo paralelním renderování komponent. Žádná extra konfigurace není potřeba.
Mohu kombinovat Zustand s Redux v jednom projektu?
Technicky ano, ale většinou to nedává smysl — jenom tím přidáváte zbytečnou složitost. Mnohem lepší a doporučený pattern je kombinovat Zustand (klientský stav) s TanStack Query (serverový stav). Pokud ale migrujete z Reduxu, můžete přecházet postupně — Zustand nevyžaduje providery, takže ho přidáte do jednotlivých částí aplikace, aniž byste museli sahat do existujícího Redux storu.
Jak řeším persistenci stavu při aktualizaci struktury storu?
Zustand persist middleware podporuje parametr version a funkci migrate. Při změně struktury storu zvýšíte verzi a v migrační funkci transformujete starý stav na nový formát. Tím zabráníte pádu aplikace při aktualizaci, kdy uživatel má v úložišti stav ve starém formátu. Je to jednodušší, než to zní.