Wybór biblioteki do zarządzania stanem to jedna z tych decyzji, które wracają do Ciebie po 6 miesiącach — albo z wdzięcznością, albo z bólem głowy. W React Native robi się to szczególnie ciekawe, bo w 2026 roku krajobraz wygląda zupełnie inaczej niż dwa lata temu. Redux przestał być domyślnym wyborem (uczciwie? Sam nie pamiętam, kiedy ostatnio założyłem nowy projekt z czystym Reduxem), a deweloperzy coraz częściej sięgają po lżejsze, hookowe rozwiązania zoptymalizowane pod urządzenia mobilne. W tym przewodniku porównam Zustand, Redux Toolkit i Jotai w kontekście React Native — z benchmarkami, gotowymi wzorcami kodu i konkretnymi wskazówkami, kiedy wybrać którą bibliotekę.
Dlaczego zarządzanie stanem w React Native wymaga innego podejścia niż React Web?
Aplikacje mobilne grają według innych zasad. Rozmiar bundla bezpośrednio przekłada się na czas pobierania ze sklepu (i na to, czy ktoś w ogóle dokończy instalację) oraz na czas zimnego startu. Pamięć operacyjna jest ograniczona — szczególnie na średniej półce Androida — a użytkownicy częściej restartują aplikację. To z kolei oznacza, że persystencja stanu (przez AsyncStorage lub MMKV) staje się obowiązkowym elementem architektury, a nie czymś, co dodaje się "kiedyś później".
Do tego dochodzi Nowa Architektura — Fabric plus TurboModules — wprowadzona na dobre w React Native 0.76+. Koszt nadmiernych re-renderów na urządzeniu spadł, ale szczerze, nie zniknął. Dlatego wybór biblioteki, która domyślnie minimalizuje re-rendery (np. Jotai z atomami) ma realne znaczenie dla płynności UI na słabszych urządzeniach Android. Sprawdziłem to osobiście na kilku Samsungach z 2020 roku i różnica jest odczuwalna.
Czego oczekujemy od biblioteki state management w 2026 roku?
- Mały bundle — najlepiej poniżej 5 KB minified+gzipped.
- Brak Provider hell — globalny stan dostępny bez owijania całej aplikacji w drzewo dostawców.
- Łatwa persystencja — natywna integracja z
AsyncStoragelubMMKV. - Wsparcie TypeScript — silna inferencja typów bez ręcznego deklarowania interfejsów dla każdego selektora.
- Praca z asynchronicznymi danymi — obsługa thunków, suspense lub natywnych async actions.
- Kompatybilność z Hermes i Nową Architekturą — bez polyfilli i hacków.
Szybkie porównanie: Zustand vs Redux Toolkit vs Jotai (2026)
Zacznijmy od ściągi. Poniższa tabela podsumowuje kluczowe różnice na bazie najnowszych danych z npm i benchmarków społeczności:
| Cecha | Zustand | Redux Toolkit | Jotai |
|---|---|---|---|
| Rozmiar bundla (gzipped) | ~3 KB | ~15 KB (z react-redux) | ~4 KB |
| Tygodniowe pobrania (npm) | ~20 mln | ~6 mln | ~2 mln |
| Boilerplate | Bardzo niski | Średni | Bardzo niski |
| Mental model | Pojedynczy store, hooki | Slice'y, reducery, akcje | Atomy + derywacje |
| Provider wymagany? | Nie | Tak | Opcjonalnie |
| DevTools | Podstawowe (przez middleware) | Najlepsze w klasie (time-travel) | Ograniczone |
| Async actions | Natywne (zwykłe funkcje) | createAsyncThunk / RTK Query | Async atomy + Suspense |
| Persystencja | persist middleware | redux-persist | atomWithStorage |
| Najlepsze zastosowanie | Małe i średnie aplikacje | Aplikacje enterprise | UI z atomowym stanem |
Benchmarki wydajnościowe (M1 MacBook, React Native 0.79, 1000 subskrybowanych komponentów)
- Pojedyncza aktualizacja stanu: Zustand: 12 ms · Jotai: 14 ms · Redux Toolkit: 18 ms
- Zużycie pamięci: Zustand: 2,1 MB · Jotai: 1,8 MB · Redux Toolkit: 3,2 MB
- Czas parsowania bundla (4× spowolniony CPU): Zustand: 8 ms · Jotai: 9 ms · Redux Toolkit: 34 ms
I tu robi się ciekawie. Różnica 12 KB między Zustand a Redux Toolkit przekłada się na ok. 100 ms wolniejszy start aplikacji w sieci 3G — nieistotne w Warszawie, ale na rynkach wschodzących to różnica między retencją a odinstalowaniem.
Zustand w React Native: prosty, szybki, domyślny wybór
Zustand stał się w 2026 roku de facto standardem dla nowych projektów React Native. Filozofia jest minimalistyczna do bólu: jeden hook, jeden store, zero providerów. API można opanować w 10 minut przy kawie — i co ważniejsze, junior w zespole też je opanuje bez tygodnia szkolenia.
Instalacja i podstawowy store
npm install zustand
npm install @react-native-async-storage/async-storage
Zdefiniujmy prosty store z licznikiem i akcją asynchroniczną pobierającą dane użytkownika z API:
// stores/useAppStore.ts
import { create } from 'zustand';
interface User {
id: string;
name: string;
email: string;
}
interface AppState {
count: number;
user: User | null;
isLoading: boolean;
error: string | null;
increment: () => void;
fetchUser: (id: string) => Promise<void>;
logout: () => void;
}
export const useAppStore = create<AppState>((set) => ({
count: 0,
user: null,
isLoading: false,
error: null,
increment: () => set((state) => ({ count: state.count + 1 })),
fetchUser: async (id) => {
set({ isLoading: true, error: null });
try {
const res = await fetch(`https://api.example.com/users/${id}`);
const user = await res.json();
set({ user, isLoading: false });
} catch (err) {
set({ error: (err as Error).message, isLoading: false });
}
},
logout: () => set({ user: null }),
}));
Użycie w komponencie React Native z selektorem
Tu jest mały haczyk, na którym potyka się większość ludzi. Kluczem do dobrej wydajności jest subskrybowanie tylko tej części stanu, która jest faktycznie potrzebna komponentowi — nie całego store.
// screens/ProfileScreen.tsx
import React, { useEffect } from 'react';
import { View, Text, ActivityIndicator, Button } from 'react-native';
import { useAppStore } from '../stores/useAppStore';
export function ProfileScreen({ userId }: { userId: string }) {
// Subskrybujemy tylko user i isLoading — count nie wywoła re-rendera
const user = useAppStore((s) => s.user);
const isLoading = useAppStore((s) => s.isLoading);
const fetchUser = useAppStore((s) => s.fetchUser);
const logout = useAppStore((s) => s.logout);
useEffect(() => {
fetchUser(userId);
}, [userId, fetchUser]);
if (isLoading) return <ActivityIndicator />;
if (!user) return <Text>Brak użytkownika</Text>;
return (
<View>
<Text>{user.name}</Text>
<Text>{user.email}</Text>
<Button title="Wyloguj" onPress={logout} />
</View>
);
}
Persystencja z AsyncStorage
Żeby stan przetrwał restart aplikacji, otaczamy creator middlewarem persist:
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import AsyncStorage from '@react-native-async-storage/async-storage';
export const useAuthStore = create(
persist<AuthState>(
(set) => ({
token: null,
setToken: (token) => set({ token }),
clear: () => set({ token: null }),
}),
{
name: 'auth-storage',
storage: createJSONStorage(() => AsyncStorage),
// Wybieramy, które pola serializować
partialize: (state) => ({ token: state.token }),
version: 1,
}
)
);
Uwaga bezpieczeństwa (poważnie, ten akapit przeczytaj dwa razy): nigdy nie zapisuj wrażliwych danych — haseł, tokenów refresh, danych biometrycznych — w AsyncStorage. Użyj expo-secure-store albo react-native-keychain. AsyncStorage jest przeznaczony do zwykłych preferencji użytkownika i niepoufnych metadanych. Widziałem aplikację, która lądowała tokeny dostępu w AsyncStorage i była "bezpieczna" przez dwa lata — do pierwszego audytu.
Slices — skalowanie Zustand w większych aplikacjach
Gdy store rośnie, dziel go na slice'y odpowiedzialne za jedną domenę. Tak, to ten sam pomysł co w Reduksie — tylko bez ceremonii:
// stores/slices/cartSlice.ts
import { StateCreator } from 'zustand';
export interface CartSlice {
items: CartItem[];
addItem: (item: CartItem) => void;
removeItem: (id: string) => void;
}
export const createCartSlice: StateCreator<
CartSlice & AuthSlice,
[],
[],
CartSlice
> = (set) => ({
items: [],
addItem: (item) => set((s) => ({ items: [...s.items, item] })),
removeItem: (id) =>
set((s) => ({ items: s.items.filter((i) => i.id !== id) })),
});
// stores/useStore.ts
export const useStore = create<CartSlice & AuthSlice>()((...a) => ({
...createCartSlice(...a),
...createAuthSlice(...a),
}));
Redux Toolkit w React Native: ekosystem i przewidywalność
Redux Toolkit (RTK) to nowoczesna twarz Redux — w 2026 roku nikt nie pisze już ręcznie reducerów ani action creatorów (a jeśli pisze, to czas na rozmowę). RTK rozwiązał historyczny problem boilerplate, ale wciąż wymaga więcej setupu niż Zustand. Wybierz RTK gdy:
- Pracujesz w zespole 10+ deweloperów i potrzebujesz wymuszonej struktury.
- Wymagasz time-travel debugging (Redux DevTools to wciąż złoty standard).
- Korzystasz z RTK Query do cachowania danych z API zamiast TanStack Query.
- Migrujesz istniejący projekt z klasycznego Redux.
Konfiguracja store i slice
// store/userSlice.ts
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
export const fetchUser = createAsyncThunk(
'user/fetch',
async (id: string, { rejectWithValue }) => {
try {
const res = await fetch(`https://api.example.com/users/${id}`);
if (!res.ok) throw new Error('Network error');
return (await res.json()) as User;
} catch (err) {
return rejectWithValue((err as Error).message);
}
}
);
const userSlice = createSlice({
name: 'user',
initialState: { data: null as User | null, status: 'idle', error: null as string | null },
reducers: {
logout(state) {
state.data = null;
},
},
extraReducers: (builder) => {
builder
.addCase(fetchUser.pending, (state) => { state.status = 'loading'; })
.addCase(fetchUser.fulfilled, (state, action: PayloadAction<User>) => {
state.status = 'idle';
state.data = action.payload;
})
.addCase(fetchUser.rejected, (state, action) => {
state.status = 'failed';
state.error = action.payload as string;
});
},
});
export const { logout } = userSlice.actions;
export default userSlice.reducer;
// store/index.ts
import { configureStore } from '@reduxjs/toolkit';
import { persistStore, persistReducer } from 'redux-persist';
import AsyncStorage from '@react-native-async-storage/async-storage';
import userReducer from './userSlice';
const persistConfig = {
key: 'root',
storage: AsyncStorage,
whitelist: ['user'],
};
const persistedReducer = persistReducer(persistConfig, userReducer);
export const store = configureStore({
reducer: { user: persistedReducer },
middleware: (getDefault) =>
getDefault({
serializableCheck: {
ignoredActions: ['persist/PERSIST', 'persist/REHYDRATE'],
},
}),
});
export const persistor = persistStore(store);
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
Provider w korzeniu aplikacji
// App.tsx
import { Provider } from 'react-redux';
import { PersistGate } from 'redux-persist/integration/react';
import { store, persistor } from './store';
export default function App() {
return (
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<RootNavigator />
</PersistGate>
</Provider>
);
}
Typowane hooki
// store/hooks.ts
import { useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux';
import type { RootState, AppDispatch } from './index';
export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
Jotai w React Native: atomowy stan dla precyzyjnych re-renderów
Jotai gra zupełnie inaczej. Zamiast pojedynczego store mamy zbiór małych, niezależnych atomów. Komponenty subskrybują tylko te atomy, których faktycznie używają — co daje bardzo precyzyjną kontrolę nad re-renderami. To świetny wybór dla rozbudowanych ekranów z formularzami albo aplikacji z dużą ilością niezależnego stanu UI. Pierwszy raz, kiedy wpiąłem Jotai do złożonego ekranu wizardu, zauważyłem różnicę gołym okiem.
Instalacja i pierwsze atomy
npm install jotai
// atoms/userAtoms.ts
import { atom } from 'jotai';
import { atomWithStorage, createJSONStorage } from 'jotai/utils';
import AsyncStorage from '@react-native-async-storage/async-storage';
// Prosty atom (efemeryczny, w pamięci)
export const counterAtom = atom(0);
// Atom z persystencją w AsyncStorage
const storage = createJSONStorage<string | null>(() => AsyncStorage);
export const tokenAtom = atomWithStorage<string | null>('auth-token', null, storage);
// Atom derywowany — zawsze synchronizuje się z tokenAtom
export const isLoggedInAtom = atom((get) => get(tokenAtom) !== null);
// Async atom — Jotai automatycznie integruje z React Suspense
export const userAtom = atom(async (get) => {
const token = get(tokenAtom);
if (!token) return null;
const res = await fetch('https://api.example.com/me', {
headers: { Authorization: `Bearer ${token}` },
});
return res.json() as Promise<User>;
});
Użycie w komponencie
// screens/HomeScreen.tsx
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
import { Suspense } from 'react';
import { ActivityIndicator, Text, View, Button } from 'react-native';
import { counterAtom, userAtom, tokenAtom, isLoggedInAtom } from '../atoms/userAtoms';
function CounterButton() {
const [count, setCount] = useAtom(counterAtom);
return <Button title={`Klikni ${count}`} onPress={() => setCount(count + 1)} />;
}
function UserBadge() {
const user = useAtomValue(userAtom); // automatyczne Suspense
return <Text>Witaj, {user?.name ?? 'gościu'}</Text>;
}
export function HomeScreen() {
const isLoggedIn = useAtomValue(isLoggedInAtom);
const setToken = useSetAtom(tokenAtom);
return (
<View>
<Suspense fallback={<ActivityIndicator />}>
{isLoggedIn ? <UserBadge /> : <Text>Zaloguj się</Text>}
</Suspense>
<CounterButton />
<Button title="Wyloguj" onPress={() => setToken(null)} />
</View>
);
}
I to jest piękne. CounterButton nie re-renderuje się przy zmianie userAtom. UserBadge nie re-renderuje się przy zmianie counterAtom. To efekt atomowej granularności — bez ręcznego pisania selektorów, bez memoizacji, bez kombinowania.
Decyzja: które rozwiązanie wybrać dla swojego projektu?
Wybierz Zustand jeśli...
- Zaczynasz nowy projekt React Native i nie chcesz tracić czasu na konfigurację.
- Twój zespół to do 5 deweloperów.
- Zależy Ci na minimalnym rozmiarze bundla i szybkim starcie aplikacji.
- Łączysz state management z TanStack Query do danych z serwera.
Wybierz Redux Toolkit jeśli...
- Pracujesz w dużym zespole i potrzebujesz wymuszonej struktury.
- Korzystasz z RTK Query do cachowania API i normalizacji danych.
- Time-travel debugging i Redux DevTools są krytyczne dla Twojego workflow.
- Migrujesz istniejący projekt Redux — RTK to ścieżka najmniejszego oporu.
Wybierz Jotai jeśli...
- Aplikacja ma dużo niezależnego stanu UI (np. zaawansowane formularze, edytory).
- Chcesz wykorzystać React Suspense do obsługi async data.
- Re-rendery na słabszych urządzeniach Android są dla Ciebie wąskim gardłem.
- Twój zespół ceni programowanie funkcyjne i kompozycję.
Hybrydowe podejście — najlepsza praktyka 2026
Tu mam mocne zdanie. W dużych aplikacjach mobilnych nie musisz wybierać jednej biblioteki — i prawdę mówiąc, nie powinieneś. Powszechny stack 2026 wygląda tak:
- TanStack Query — stan serwera (cache, refetch, mutacje, optimistic updates).
- Zustand — stan globalny aplikacji (auth, ustawienia, koszyk).
- Jotai — stan lokalny ekranu (formularze, edytory, modal state).
Każde narzędzie do swojej domeny. Próba użycia jednej biblioteki do wszystkiego prowadzi do nadmiernej złożoności tam, gdzie wystarczyłby zwykły useState, oraz do duplikowania logiki cachowania, którą TanStack Query rozwiązuje w 10 linijkach.
Wskazówki wydajnościowe specyficzne dla React Native
1. Używaj MMKV zamiast AsyncStorage dla często czytanego stanu
react-native-mmkv jest synchroniczny i ~30× szybszy od AsyncStorage. Wszystkie trzy biblioteki obsługują MMKV jako backend persystencji — wystarczy chwila konfiguracji:
import { MMKV } from 'react-native-mmkv';
import { StateStorage } from 'zustand/middleware';
const mmkv = new MMKV();
const mmkvStorage: StateStorage = {
getItem: (name) => mmkv.getString(name) ?? null,
setItem: (name, value) => mmkv.set(name, value),
removeItem: (name) => mmkv.delete(name),
};
2. Dziel store/atomy po feature, nie po typie
Zamiast jednego globalnego store ze wszystkim, twórz osobne sklepy dla niezależnych domen — auth, koszyk, ustawienia. Mniejsze sklepy = mniej re-renderów i znacznie łatwiejsze testowanie. To proste, ale łatwe do przegapienia, gdy projekt rośnie organicznie.
3. Nigdy nie mutuj stanu bezpośrednio
// Źle — nie wywoła re-rendera
state.items.push(newItem);
// Dobrze
set((s) => ({ items: [...s.items, newItem] }));
Redux Toolkit pozwala na pozorną mutację dzięki Immer pod spodem — to wyjątek od reguły, ale nadal warto rozumieć, że pod maską tworzy nową referencję.
4. Pamiętaj o Hermes i Nowej Architekturze
Wszystkie trzy biblioteki działają poprawnie z Hermesem i Fabric od React Native 0.76+. Jeśli korzystasz z React Native 0.84+ (luty 2026), masz pełną kompatybilność out-of-the-box. Bez polyfilli, bez hacków, bez konfiguracji babel-loader o trzeciej w nocy.
FAQ — najczęściej zadawane pytania
Czy Zustand zastąpił Redux w React Native?
W nowych projektach — w dużej mierze tak. Zustand ma ~20 mln tygodniowych pobrań i jest najpopularniejszym wyborem dla świeżo zakładanych aplikacji React Native. Redux Toolkit pozostaje jednak dominujący w istniejących, dużych aplikacjach enterprise oraz tam, gdzie kluczowe są time-travel debugging i RTK Query.
Czy mogę używać Zustand i Redux Toolkit w tej samej aplikacji?
Technicznie tak — biblioteki nie kolidują. Czasami ma to sens podczas stopniowej migracji z Redux na Zustand. Docelowo jednak utrzymywanie dwóch rozwiązań do tego samego celu zwiększa złożoność i ryzyko desynchronizacji stanu, więc warto zaplanować pełną migrację (i trzymać się planu).
Czy Context API wystarczy zamiast biblioteki state management?
Tak, jeśli stan globalny ogranicza się do kilku rzadko zmienianych wartości — np. theme, język, zalogowany użytkownik. Context API w React Native staje się problemem, gdy stan zmienia się często: każda zmiana powoduje re-render wszystkich konsumentów. Wtedy biblioteka z selektorami (Zustand) lub atomami (Jotai) jest znacznie wydajniejsza.
Czy Jotai działa z React Native Suspense i Nową Architekturą?
Tak. Jotai natywnie wspiera React Suspense dla async atomów i jest w pełni kompatybilny z Fabric oraz Hermes. To jeden z najmocniejszych argumentów za Jotai dla aplikacji RN 0.79+.
Co jest szybsze: AsyncStorage czy MMKV do persystencji stanu?
MMKV jest około 30× szybsze od AsyncStorage i działa synchronicznie, dzięki czemu nadaje się do hydratacji stanu już w pierwszym renderze. Wszystkie trzy omówione biblioteki obsługują MMKV przez customowy storage adapter — jeśli persystujesz dużo lub często, zdecydowanie warto przejść z AsyncStorage na MMKV.
Jak debugować stan w React Native?
Dla Redux Toolkit — Redux DevTools przez React Native DevTools (od deprecation Flippera w RN 0.79). Dla Zustand — middleware devtools z paczki zustand/middleware integruje się z Redux DevTools. Dla Jotai — jotai-devtools oraz hook useAtomsDebugValue(). Wszystkie podejścia działają z wbudowanym debuggerem React Native DevTools.
Podsumowanie
W 2026 roku zarządzanie stanem w React Native to świadomy wybór, nie odruchowe sięganie po Redux. Dla większości nowych projektów Zustand daje najlepszy balans między prostotą, wydajnością i rozmiarem bundla. Redux Toolkit wciąż króluje w aplikacjach enterprise i tam, gdzie potrzebne są zaawansowane DevTools oraz RTK Query. Jotai to wyspecjalizowane narzędzie dla aplikacji o złożonym, atomowym stanie UI i miłośników programowania funkcyjnego.
Najlepsze decyzje architektoniczne to te, które łączą silne strony różnych narzędzi — TanStack Query do stanu serwera, Zustand do globalnego stanu klienta, Jotai do lokalnego stanu ekranu. Zacznij prosto, mierz wydajność na realnych urządzeniach (nie tylko na swoim M-procesorze!) i ewoluuj architekturę w miarę wzrostu aplikacji.