Úvod: Prečo je správa stavu v React Native v roku 2026 iná
Správa stavu bola vždy jednou z najdiskutovanejších tém v React ekosystéme. Ale ruku na srdce — v roku 2026 sa situácia dramaticky zmenila. Éra, kedy ste úplne všetko (od lokálneho stavu formulárov až po serverové dáta) hádzali do jedného masívneho Redux store, je definitívne za nami. A úprimne? Je to oslobodzujúce.
Moderný prístup rozlišuje medzi rôznymi typmi stavu a pre každý používa najvhodnejší nástroj.
V tomto sprievodcovi si prejdeme tri knižnice, ktoré dominujú správe stavu v React Native aplikáciách: Zustand pre zdieľaný klientský stav, Jotai pre atomický stav s minimálnymi prerenderovaniami a TanStack Query (React Query) pre serverový stav. Ukážeme si, kedy ktorú použiť, ako ich nastaviť v Expo projekte, ako ich kombinovať a ako implementovať perzistenciu s MMKV pre offline funkčnosť.
Pochopenie typov stavu v mobilných aplikáciách
Predtým, než sa ponoríme do konkrétnych knižníc, treba pochopiť, aké typy stavu v mobilnej aplikácii vlastne existujú. Toto rozdelenie vám ušetrí kopec problémov pri výbere správneho nástroja.
Lokálny stav komponentu
Stav, ktorý žije iba v rámci jedného komponentu — otvorenie modálu, hodnota textového vstupu, stav animácie. Pre toto je klasický useState a useReducer úplne postačujúci. Žiadna externá knižnica nie je potrebná.
Zdieľaný klientský stav
Stav prístupný viacerým komponentom naprieč aplikáciou — informácie o prihlásenom používateľovi, nastavenia, nákupný košík alebo téma (svetlá/tmavá). Tu vynikajú Zustand a Jotai.
Serverový stav
Dáta zo servera — zoznamy produktov, detaily objednávok, notifikácie. Tieto dáta majú vlastné životné cykly (načítavanie, kešovanie, invalidácia, refetching) a pre ich správu je TanStack Query bezkonkurenčne najlepšou voľbou.
Navigačný stav
Stav súvisiaci s navigáciou — aktuálna obrazovka, parametre routy, história. Toto rieši React Navigation alebo Expo Router a nie je predmetom tohto článku.
Zustand: Elegantná jednoduchosť pre zdieľaný stav
Zustand (z nemčiny — „stav") sa stal jednou z najpopulárnejších knižníc v React ekosystéme. S medziročným rastom adopcie viac ako 30 % je prítomný v približne 40 % nových React Native projektov. Dôvod je jednoduchý — minimálny boilerplate, intuitívne API a výkon, ktorý vás nesklamie.
Inštalácia a základné nastavenie
Začnime inštaláciou v Expo projekte:
npx expo install zustand
Vytvorenie prvého store je neuveriteľne jednoduché (a keď hovorím neuveriteľne, myslím to vážne):
// stores/useAuthStore.ts
import { create } from 'zustand';
interface User {
id: string;
name: string;
email: string;
avatar?: string;
}
interface AuthState {
user: User | null;
isAuthenticated: boolean;
token: string | null;
login: (user: User, token: string) => void;
logout: () => void;
updateProfile: (updates: Partial<User>) => void;
}
export const useAuthStore = create<AuthState>((set) => ({
user: null,
isAuthenticated: false,
token: null,
login: (user, token) =>
set({
user,
isAuthenticated: true,
token,
}),
logout: () =>
set({
user: null,
isAuthenticated: false,
token: null,
}),
updateProfile: (updates) =>
set((state) => ({
user: state.user ? { ...state.user, ...updates } : null,
})),
}));
Použitie v komponentoch je rovnako priamočiare:
// screens/ProfileScreen.tsx
import { View, Text, Pressable } from 'react-native';
import { useAuthStore } from '../stores/useAuthStore';
export function ProfileScreen() {
const user = useAuthStore((state) => state.user);
const logout = useAuthStore((state) => state.logout);
if (!user) return null;
return (
<View style={{ flex: 1, padding: 16 }}>
<Text style={{ fontSize: 24, fontWeight: 'bold' }}>
{user.name}
</Text>
<Text style={{ color: '#666', marginTop: 4 }}>
{user.email}
</Text>
<Pressable
onPress={logout}
style={{
marginTop: 24,
backgroundColor: '#ef4444',
padding: 12,
borderRadius: 8,
alignItems: 'center',
}}
>
<Text style={{ color: 'white', fontWeight: '600' }}>
Odhlásiť sa
</Text>
</Pressable>
</View>
);
}
Selektory a optimalizácia renderovania
Jedna z najdôležitejších vecí pri práci so Zustandom je selektívne odoberanie stavu. Všimnite si, že v predchádzajúcom príklade sme nepoužili const { user, logout } = useAuthStore(), ale vybrali sme každú hodnotu samostatne. Toto je kľúčové pre výkon — komponent sa prerenderuje iba vtedy, keď sa zmení konkrétna hodnota, ktorú odoberá.
// ÁNO - komponent sa prerenderuje iba keď sa zmení 'user'
const user = useAuthStore((state) => state.user);
// NIE - komponent sa prerenderuje pri KAŽDEJ zmene v store
const { user, isAuthenticated, token } = useAuthStore();
// Pre výber viacerých hodnôt použite shallow porovnávanie
import { useShallow } from 'zustand/react/shallow';
const { user, isAuthenticated } = useAuthStore(
useShallow((state) => ({
user: state.user,
isAuthenticated: state.isAuthenticated,
}))
);
Toto je chyba, ktorú vidím veľmi často. Ľudia destrukturujú celý store a potom sa čudujú, prečo im komponent bliká pri každej zmene.
Perzistencia s MMKV — bleskovo rýchle offline úložisko
V mobilných appkách je perzistencia stavu nevyhnutná. Používateľ jednoducho očakáva, že po reštarte aplikácie zostane prihlásený, nastavenia ostanú zachované a košík sa nestratí. Zustand ponúka persist middleware, ktorý v kombinácii s react-native-mmkv poskytuje extrémne rýchle úložisko — benchmarky ukazujú, že MMKV je 30 až 100-krát rýchlejšie ako AsyncStorage. Nie je to preklep.
// lib/storage.ts
import { MMKV } from 'react-native-mmkv';
import { StateStorage } from 'zustand/middleware';
export const mmkvStorage = new MMKV();
export const zustandMMKVStorage: StateStorage = {
setItem: (name, value) => {
mmkvStorage.set(name, value);
},
getItem: (name) => {
return mmkvStorage.getString(name) ?? null;
},
removeItem: (name) => {
mmkvStorage.delete(name);
},
};
// stores/useSettingsStore.ts
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import { zustandMMKVStorage } from '../lib/storage';
interface SettingsState {
theme: 'light' | 'dark' | 'system';
language: string;
notificationsEnabled: boolean;
setTheme: (theme: 'light' | 'dark' | 'system') => void;
setLanguage: (language: string) => void;
toggleNotifications: () => void;
}
export const useSettingsStore = create<SettingsState>()(
persist(
(set) => ({
theme: 'system',
language: 'sk',
notificationsEnabled: true,
setTheme: (theme) => set({ theme }),
setLanguage: (language) => set({ language }),
toggleNotifications: () =>
set((state) => ({
notificationsEnabled: !state.notificationsEnabled,
})),
}),
{
name: 'app-settings',
storage: createJSONStorage(() => zustandMMKVStorage),
}
)
);
Pokročilé vzory: Slices a modulárna architektúra
Pre väčšie aplikácie je rozumné rozdeliť store do logických celkov — takzvaných slices. Zustand toto podporuje elegantne:
// stores/slices/cartSlice.ts
import { StateCreator } from 'zustand';
export interface CartItem {
id: string;
name: string;
price: number;
quantity: number;
}
export interface CartSlice {
items: CartItem[];
addItem: (item: Omit<CartItem, 'quantity'>) => void;
removeItem: (id: string) => void;
updateQuantity: (id: string, quantity: number) => void;
clearCart: () => void;
getTotal: () => number;
}
export const createCartSlice: StateCreator<CartSlice> = (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 } : i
),
})),
clearCart: () => set({ items: [] }),
getTotal: () =>
get().items.reduce(
(total, item) => total + item.price * item.quantity,
0
),
});
Jotai: Atomický stav pre maximálny výkon
Zatiaľ čo Zustand pracuje s centralizovanými store, Jotai prináša úplne odlišný prístup — atomický stav. Každý kúsok stavu je samostatný atóm a komponenty sa prerenderujú iba vtedy, keď sa zmení konkrétny atóm, ktorý používajú. Ak poznáte Recoil alebo koncept signálov, budete sa cítiť ako doma.
Inštalácia a základný koncept
npx expo install jotai
Základná jednotka Jotai je atóm — jednoduchý kontajner pre kúsok stavu:
// atoms/counterAtoms.ts
import { atom } from 'jotai';
// Primitívny atóm — obsahuje jednoduchú hodnotu
export const countAtom = atom(0);
// Odvodený (derived) atóm — vypočítaný z iných atómov
export const doubleCountAtom = atom((get) => get(countAtom) * 2);
// Atóm s read/write — vlastná logika pre čítanie aj zápis
export const countWithMinMaxAtom = atom(
(get) => get(countAtom),
(get, set, newValue: number) => {
const clamped = Math.max(0, Math.min(100, newValue));
set(countAtom, clamped);
}
);
Použitie atómov v komponentoch pripomína klasický useState, čo je na tom to pekné:
// components/Counter.tsx
import { View, Text, Pressable, StyleSheet } from 'react-native';
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
import { countAtom, doubleCountAtom } from '../atoms/counterAtoms';
export function Counter() {
const [count, setCount] = useAtom(countAtom);
const doubleCount = useAtomValue(doubleCountAtom);
return (
<View style={styles.container}>
<Text style={styles.count}>Počet: {count}</Text>
<Text style={styles.derived}>Dvojnásobok: {doubleCount}</Text>
<View style={styles.buttons}>
<Pressable
onPress={() => setCount((c) => c - 1)}
style={styles.button}
>
<Text style={styles.buttonText}>-</Text>
</Pressable>
<Pressable
onPress={() => setCount((c) => c + 1)}
style={styles.button}
>
<Text style={styles.buttonText}>+</Text>
</Pressable>
</View>
</View>
);
}
// Tento komponent sa NEPRERENDERUJE pri zmene countAtom
export function UnrelatedComponent() {
return <Text>Tento komponent sa nikdy neprerenderuje</Text>;
}
Optimalizácia výkonu s useSetAtom
Jotai ponúka hook useSetAtom, ktorý je kľúčový pre výkon. Keď komponent potrebuje iba zapisovať do atómu (ale nepotrebuje čítať jeho hodnotu), useSetAtom zabráni zbytočným prerenderovaniam. Toto je detail, ktorý môže urobiť veľký rozdiel v zoznamoch s desiatkami položiek.
// components/AddToCartButton.tsx
import { useSetAtom } from 'jotai';
import { cartItemsAtom } from '../atoms/cartAtoms';
// Tento komponent sa NIKDY neprerenderuje pri zmene košíka
export function AddToCartButton({ product }) {
const addItem = useSetAtom(cartItemsAtom);
return (
<Pressable onPress={() => addItem([...prev, product])}>
<Text>Pridať do košíka</Text>
</Pressable>
);
}
Atomická rodina — dynamické atómy
Pre scenáre, kde potrebujete dynamicky vytvárať atómy (napríklad pre zoznam položiek), Jotai ponúka atomFamily:
// atoms/todoAtoms.ts
import { atom } from 'jotai';
import { atomFamily } from 'jotai/utils';
interface Todo {
id: string;
text: string;
completed: boolean;
}
// Zoznam ID všetkých úloh
export const todoIdsAtom = atom<string[]>([]);
// Rodina atómov — každá úloha má vlastný atóm
export const todoAtomFamily = atomFamily((id: string) =>
atom<Todo>({
id,
text: '',
completed: false,
})
);
// Odvodený atóm pre počet nesplnených úloh
export const incompleteTodosCountAtom = atom((get) => {
const ids = get(todoIdsAtom);
return ids.filter((id) => !get(todoAtomFamily(id)).completed).length;
});
Krása tohto prístupu spočíva v tom, že keď zmeníte jednu úlohu, prerenderuje sa iba komponent zobrazujúci tú konkrétnu úlohu. Ostatné položky v zozname zostanú nedotknuté.
Perzistencia Jotai atómov s MMKV
Jotai podporuje perzistenciu cez utilitu atomWithStorage. Pre React Native si vytvoríme vlastný MMKV adaptér:
// lib/jotaiStorage.ts
import { createJSONStorage } from 'jotai/utils';
import { MMKV } from 'react-native-mmkv';
const mmkv = new MMKV();
export const mmkvJotaiStorage = createJSONStorage<any>(() => ({
getItem: (key: string) => {
const value = mmkv.getString(key);
return value ?? null;
},
setItem: (key: string, value: string) => {
mmkv.set(key, value);
},
removeItem: (key: string) => {
mmkv.delete(key);
},
}));
// atoms/userPreferences.ts
import { atomWithStorage } from 'jotai/utils';
import { mmkvJotaiStorage } from '../lib/jotaiStorage';
export const themeAtom = atomWithStorage(
'user-theme',
'system',
mmkvJotaiStorage
);
export const onboardingCompletedAtom = atomWithStorage(
'onboarding-completed',
false,
mmkvJotaiStorage
);
TanStack Query: Majster serverového stavu
Tak, poďme na TanStack Query (predtým React Query). Toto je špecializovaná knižnica pre správu serverového stavu. Ak vaša aplikácia komunikuje s API — načítava zoznamy, odosiela formuláre, synchronizuje dáta — toto je nástroj, ktorý potrebujete. Podľa mojich skúseností rieši až 80 % všetkých stavových vzorcov v dátovo náročných aplikáciách.
Prečo nestačí len Zustand alebo Jotai?
Serverový stav je zásadne odlišný od klientského:
- Je vzdialený — dáta existujú na serveri a vy máte iba lokálnu kópiu.
- Môže byť zastaraný — iný používateľ ho mohol medzitým zmeniť.
- Vyžaduje kešovanie — nechcete načítavať tie isté dáta pri každom zobrazení obrazovky.
- Má životný cyklus — načítavanie, úspech, chyba, obnovenie.
- Potrebuje invalidáciu — po mutácii treba obnoviť súvisiace dáta.
TanStack Query rieši všetky tieto problémy so stratégiou stale-while-revalidate — zobrazí kešované dáta okamžite a na pozadí ich obnoví. Je to jednoducho geniálne.
Inštalácia a konfigurácia
npx expo install @tanstack/react-query
Nastavenie v hlavnom súbore aplikácie:
// app/_layout.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { Stack } from 'expo-router';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 minút
gcTime: 1000 * 60 * 30, // 30 minút
retry: 3,
retryDelay: (attemptIndex) =>
Math.min(1000 * 2 ** attemptIndex, 30000),
},
},
});
export default function RootLayout() {
return (
<QueryClientProvider client={queryClient}>
<Stack />
</QueryClientProvider>
);
}
Queries — načítavanie dát
Základom TanStack Query sú queries — deklaratívne definície pre načítavanie dát:
// hooks/useProducts.ts
import { useQuery } from '@tanstack/react-query';
interface Product {
id: string;
name: string;
price: number;
category: string;
imageUrl: string;
}
async function fetchProducts(category?: string): Promise<Product[]> {
const url = category
? `https://api.example.com/products?category=${category}`
: 'https://api.example.com/products';
const response = await fetch(url);
if (!response.ok) {
throw new Error('Nepodarilo sa načítať produkty');
}
return response.json();
}
export function useProducts(category?: string) {
return useQuery({
queryKey: ['products', { category }],
queryFn: () => fetchProducts(category),
staleTime: 1000 * 60 * 10, // 10 minút
});
}
A takto to vyzerá v praxi — kompletná obrazovka so zoznamom produktov vrátane pull-to-refresh:
// screens/ProductListScreen.tsx
import {
View,
Text,
FlatList,
ActivityIndicator,
RefreshControl,
} from 'react-native';
import { useProducts } from '../hooks/useProducts';
export function ProductListScreen() {
const {
data: products,
isLoading,
isError,
error,
refetch,
isRefetching,
} = useProducts();
if (isLoading) {
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<ActivityIndicator size="large" />
<Text style={{ marginTop: 8 }}>Načítavam produkty...</Text>
</View>
);
}
if (isError) {
return (
<View style={{ flex: 1, justifyContent: 'center', padding: 16 }}>
<Text style={{ color: '#ef4444', textAlign: 'center' }}>
Chyba: {error.message}
</Text>
</View>
);
}
return (
<FlatList
data={products}
keyExtractor={(item) => item.id}
refreshControl={
<RefreshControl
refreshing={isRefetching}
onRefresh={refetch}
/>
}
renderItem={({ item }) => (
<View style={{ padding: 16, borderBottomWidth: 1, borderColor: '#eee' }}>
<Text style={{ fontSize: 16, fontWeight: '600' }}>
{item.name}
</Text>
<Text style={{ color: '#666' }}>
{item.price.toFixed(2)} €
</Text>
</View>
)}
/>
);
}
Mutations — odosielanie dát na server
Pre operácie, ktoré menia dáta na serveri (vytváranie, úprava, mazanie), sú tu mutations:
// hooks/useCreateOrder.ts
import { useMutation, useQueryClient } from '@tanstack/react-query';
interface CreateOrderInput {
items: Array<{ productId: string; quantity: number }>;
shippingAddress: string;
}
async function createOrder(input: CreateOrderInput) {
const response = await fetch('https://api.example.com/orders', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(input),
});
if (!response.ok) {
throw new Error('Nepodarilo sa vytvoriť objednávku');
}
return response.json();
}
export function useCreateOrder() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: createOrder,
onSuccess: () => {
// Invaliduje kešované objednávky — vyvolá refetch
queryClient.invalidateQueries({ queryKey: ['orders'] });
// Invaliduje aj produkty (napr. aktualizácia stavu skladu)
queryClient.invalidateQueries({ queryKey: ['products'] });
},
onError: (error) => {
console.error('Chyba pri vytváraní objednávky:', error);
},
});
}
Optimistické aktualizácie
Pre lepší používateľský zážitok môžeme implementovať optimistické aktualizácie — UI sa aktualizuje okamžite, ešte pred potvrdením zo servera. Používateľ tak nemusí čakať a aplikácia pôsobí rýchlejšie:
// hooks/useToggleFavorite.ts
import { useMutation, useQueryClient } from '@tanstack/react-query';
export function useToggleFavorite() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (productId: string) =>
fetch(`https://api.example.com/favorites/${productId}`, {
method: 'POST',
}),
onMutate: async (productId) => {
// Zrušiť prebiehajúce refetche
await queryClient.cancelQueries({
queryKey: ['products'],
});
// Uložiť predchádzajúci stav
const previousProducts = queryClient.getQueryData(['products']);
// Optimisticky aktualizovať keš
queryClient.setQueryData(['products'], (old: Product[]) =>
old.map((p) =>
p.id === productId
? { ...p, isFavorite: !p.isFavorite }
: p
)
);
return { previousProducts };
},
onError: (_error, _productId, context) => {
// Pri chybe obnoviť predchádzajúci stav
queryClient.setQueryData(
['products'],
context?.previousProducts
);
},
onSettled: () => {
// Vždy invalidovať pre istotu
queryClient.invalidateQueries({ queryKey: ['products'] });
},
});
}
Offline podpora v React Native
TanStack Query v5 ponúka robustnú offline podporu. V prehliadači sa sieťový stav detekuje automaticky, no v React Native to treba nastaviť manuálne:
// lib/queryOnlineManager.ts
import NetInfo from '@react-native-community/netinfo';
import { onlineManager } from '@tanstack/react-query';
// Nastavenie online managera pre React Native
onlineManager.setEventListener((setOnline) => {
return NetInfo.addEventListener((state) => {
setOnline(!!state.isConnected);
});
});
// lib/queryFocusManager.ts
import { focusManager } from '@tanstack/react-query';
import { AppState } from 'react-native';
import type { AppStateStatus } from 'react-native';
// Automatické obnovenie dát pri návrate do aplikácie
function onAppStateChange(status: AppStateStatus) {
focusManager.setFocused(status === 'active');
}
AppState.addEventListener('change', onAppStateChange);
Pre úplnú offline funkčnosť s perzistenciou keše do MMKV:
// lib/queryPersister.ts
import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister';
import { MMKV } from 'react-native-mmkv';
const mmkv = new MMKV({ id: 'query-cache' });
const mmkvStorageAdapter = {
getItem: (key: string) => mmkv.getString(key) ?? null,
setItem: (key: string, value: string) => mmkv.set(key, value),
removeItem: (key: string) => mmkv.delete(key),
};
export const queryPersister = createSyncStoragePersister({
storage: mmkvStorageAdapter,
});
// app/_layout.tsx
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client';
import { queryPersister } from '../lib/queryPersister';
export default function RootLayout() {
return (
<PersistQueryClientProvider
client={queryClient}
persistOptions={{ persister: queryPersister }}
>
<Stack />
</PersistQueryClientProvider>
);
}
Kombinácia všetkých troch knižníc: Praktická architektúra
A tu sa to celé spája dohromady. Skutočná sila modernej správy stavu spočíva v kombinácii týchto nástrojov — každý má svoju silnú stránku a spoločne pokrývajú všetky stavové potreby mobilnej aplikácie.
Pozrime sa na praktický príklad e-commerce aplikácie.
Rozdelenie zodpovedností
// Štruktúra projektu
src/
├── atoms/ # Jotai atómy — granulárny UI stav
│ ├── filterAtoms.ts
│ └── uiAtoms.ts
├── stores/ # Zustand stores — zdieľaný klientský stav
│ ├── useAuthStore.ts
│ ├── useCartStore.ts
│ └── useSettingsStore.ts
├── hooks/ # TanStack Query hooks — serverový stav
│ ├── useProducts.ts
│ ├── useOrders.ts
│ └── useCreateOrder.ts
└── lib/
├── storage.ts # MMKV konfigurácia
└── queryClient.ts
Integrácia v praxi
Typická obrazovka môže využívať všetky tri knižnice naraz. A viete čo? Funguje to prekvapivo prehľadne:
// screens/ShopScreen.tsx
import { View, FlatList, TextInput } from 'react-native';
import { useAtom } from 'jotai';
import { useProducts } from '../hooks/useProducts';
import { useCartStore } from '../stores/useCartStore';
import {
searchQueryAtom,
selectedCategoryAtom,
} from '../atoms/filterAtoms';
import { ProductCard } from '../components/ProductCard';
export function ShopScreen() {
// Jotai — lokálny UI stav (filtre)
const [searchQuery, setSearchQuery] = useAtom(searchQueryAtom);
const [selectedCategory] = useAtom(selectedCategoryAtom);
// TanStack Query — serverové dáta
const { data: products, isLoading } = useProducts(selectedCategory);
// Zustand — klientský stav (košík)
const addItem = useCartStore((state) => state.addItem);
// Filtrovanie na klientovi
const filteredProducts = products?.filter((p) =>
p.name.toLowerCase().includes(searchQuery.toLowerCase())
);
return (
<View style={{ flex: 1 }}>
<TextInput
value={searchQuery}
onChangeText={setSearchQuery}
placeholder="Hľadať produkty..."
style={{
margin: 16,
padding: 12,
borderRadius: 8,
backgroundColor: '#f3f4f6',
}}
/>
<FlatList
data={filteredProducts}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<ProductCard
product={item}
onAddToCart={() => addItem(item)}
/>
)}
/>
</View>
);
}
Kedy použiť ktorú knižnicu: Rozhodovací sprievodca
Toto je asi najdôležitejšia časť celého článku. Pri rozhodovaní, ktorú knižnicu zvoliť pre konkrétny scenár, sa riaďte nasledujúcim.
Zustand — keď potrebujete:
- Zdieľaný stav medzi viacerými obrazovkami (prihlásený používateľ, nastavenia, košík)
- Jednoduchý, centralizovaný prístup s minimálnym boilerplatom
- Rýchlu perzistenciu s MMKV middleware
- Prístup k stavu mimo React komponentov (v navigačných guards alebo interceptoroch)
- Modulárnu architektúru so slices pre väčšie aplikácie
Jotai — keď potrebujete:
- Maximálne granulárne prerenderovanie — každý atóm spúšťa iba tie komponenty, ktoré ho priamo používajú
- Zložité odvodené stavy (derived atoms) s automatickým sledovaním závislostí
- Dynamicky vytvárané skupiny stavov (atomFamily pre zoznamy položiek)
- Inkrementálne pridávanie stavového manažmentu bez veľkej refaktorizácie
- Bottom-up prístup, kde stav rastie organicky podľa potrieb
TanStack Query — keď potrebujete:
- Načítavanie, kešovanie a synchronizáciu serverových dát
- Automatické obnovenie dát (stale-while-revalidate, refetch on focus)
- Optimistické aktualizácie pre okamžitú odozvu UI
- Stránkovanie a nekonečné scrollovanie
- Offline podporu s perzistentnou kešou
- Automatické opakovanie neúspešných požiadaviek
Porovnanie výkonu a veľkosti balíkov
Pre mobilné aplikácie je veľkosť balíka kritická — ovplyvňuje čas sťahovania, inštalácie aj rýchlosť studeného štartu. Tak si porovnajme naše tri knižnice:
- Zustand: ~2.9 kB (minified + gzipped) — jedna z najmenších stavových knižníc vôbec
- Jotai: ~3.7 kB (minified + gzipped) — minimálna veľkosť pre atómový prístup
- TanStack Query: ~12.4 kB (minified + gzipped) — väčšia, ale obsahuje kompletný kešovací systém
- Pre porovnanie: Redux Toolkit: ~13.5 kB + Redux ~4.6 kB
Celkovo kombinácia Zustand + TanStack Query (~15.3 kB) alebo Jotai + TanStack Query (~16.1 kB) je porovnateľná s Redux Toolkit (~18.1 kB), ale ponúka oveľa lepšiu ergonómiu a špecializáciu. Takže nie je dôvod sa báť prechodu.
Migrácia z Reduxu: Praktický postup
Mnohé existujúce React Native projekty stále používajú Redux. Dobrá správa — migrácia nemusí byť „všetko alebo nič". Môžete ju robiť postupne a oba systémy môžu pokojne koexistovať.
Krok 1: Identifikujte typy stavu
Prejdite si existujúce Redux slices a rozdeľte ich podľa typu. Serverové dáta (produkty, používatelia, objednávky) patria do TanStack Query. Klientský stav (auth, nastavenia, košík) patrí do Zustand. A UI stav (filtre, modály, formuláre) patrí do Jotai alebo lokálneho useState.
Krok 2: Začnite s TanStack Query
Najväčší okamžitý prínos získate migráciou serverových dát. Väčšina Redux kódu v typickej aplikácii sa totiž týka práve načítavania a kešovania dát zo servera — a TanStack Query toto rieši automaticky. Pozrite sa na ten rozdiel:
// PRED: Redux (products slice + thunk)
// productsSlice.ts - 80+ riadkov kódu
const productsSlice = createSlice({
name: 'products',
initialState: {
items: [],
loading: false,
error: null,
},
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchProducts.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(fetchProducts.fulfilled, (state, action) => {
state.loading = false;
state.items = action.payload;
})
.addCase(fetchProducts.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message;
});
},
});
// PO: TanStack Query - 15 riadkov
export function useProducts(category?: string) {
return useQuery({
queryKey: ['products', { category }],
queryFn: () => fetchProducts(category),
});
}
Z 80+ riadkov na 15. Úprimne, toto bol pre mňa ten moment, kedy som si povedal — Redux pre serverové dáta už nikdy.
Krok 3: Migrujte klientský stav na Zustand
Po migrácii serverových dát zostanú v Reduxe iba klientské slices. Tie postupne presuňte do Zustand stores. Žiadny veľký bang refaktoring — jeden slice po druhom.
Časté chyby a anti-patterny
Na záver si prejdime najčastejšie chyby, ktorých sa vývojári dopúšťajú. Niektoré z nich som urobil aj ja, takže verte, hovorím z praxe.
1. Ukladanie serverových dát do Zustand/Jotai
Nesnažte sa manuálne kešovať serverové dáta v Zustand alebo Jotai. TanStack Query toto robí automaticky a lepšie — s invalidáciou, stale-while-revalidate, retry logikou a offline podporou. Naozaj, nechajte to na špecialistu.
2. Príliš veľký globálny stav
Nie všetko musí byť v globálnom store. Stav, ktorý patrí iba jednému komponentu alebo obrazovke, by mal zostať lokálny s useState. Jednoduchšie je lepšie.
3. Chýbajúce selektory v Zustand
Vždy používajte selektory na výber konkrétnych hodnôt. Bez nich sa komponent prerenderuje pri každej zmene v store, čo môže výrazne zhoršiť výkon. Spomínali sme to vyššie, ale oplatí sa to zopakovať.
4. Ignorovanie query keys v TanStack Query
Query keys sú kľúčové pre správne kešovanie a invalidáciu. Vždy zahrňte všetky parametre, od ktorých závisí výsledok, do query key. Ak na to zabudnete, čakajú vás záhadné bugy s kešovaním.
5. Nepoužívanie MMKV pre perzistenciu
AsyncStorage prechádza cez JSON bridge a je výrazne pomalší. Pre React Native aplikácie v roku 2026 je MMKV štandardnou voľbou — je 30 až 100-krát rýchlejšie. Nie je dôvod ho nepoužiť.
Záver: Budúcnosť správy stavu v React Native
Správa stavu v React Native prešla za posledné roky dramatickou transformáciou. Od monolitického Reduxu sme sa posunuli k špecializovaným nástrojom, kde každá knižnica exceluje vo svojej oblasti. Zustand prináša elegantný prístup k zdieľanému stavu. Jotai ponúka granulárnu kontrolu nad rerenderovaním. A TanStack Query mení spôsob, akým pracujeme so serverovými dátami.
Kľúčom k úspechu nie je vybrať si jednu „najlepšiu" knižnicu, ale pochopiť typy stavu vo vašej aplikácii a pre každý použiť ten najvhodnejší nástroj. Kombinácia Zustand + TanStack Query pokryje väčšinu prípadov. Ak navyše potrebujete maximálne granulárne prerenderovanie, pridajte Jotai.
S Novou architektúrou React Native a Expo SDK 55 je ekosystém pripravený na tieto moderné vzory. Či už začínate nový projekt alebo modernizujete existujúcu appku, investícia do správnej stavovej architektúry sa vám vráti — v podobe lepšieho výkonu, čistejšieho kódu a spokojnejších používateľov.