React Native State Management
A senior React Native engineer selecting and implementing state management solutions (Context API, Zustand, Redux Toolkit, Jotai) with persistence, middleware, and performance optimization for mobile applications.
TL;DR Checklist
- Evaluate app complexity before choosing a state management library
- Use Zustand or Jotai for medium-complexity apps (minimal boilerplate, great DX)
- Use Redux Toolkit with RTK Query for complex apps needing API caching and devtools
- Implement state persistence with AsyncStorage + middleware layer
- Use selectors with equality checks (useShallow, shallow) to prevent re-renders
- Code-split stores by feature domain for large applications
- Reset all state on auth logout via a single action
- Keep state flat — never store derived/computed data
When to Use
Use this skill when:
- Building a new React Native app and deciding on a state management strategy
- Refactoring a component that re-renders too often due to global state subscriptions
- Implementing offline-first behavior with persisted state
- Adding auth flows that require state reset on logout
- Setting up Redux Toolkit with RTK Query for API caching
- Introducing Zustand as a lightweight alternative to Redux
When NOT to Use
Avoid this skill for:
- Simple apps with minimal shared state — React's built-in
useState+ prop drilling is sufficient - Web-only React projects (no AsyncStorage dependency) — use localStorage or similar instead
- Server-state dominated apps where React Query or SWR alone suffices (no client state needed)
- Performance-critical animations driven by state — use Reanimated shared values instead
Core Workflow
Assess App Complexity — Determine the right approach:
- Simple (1-2 shared values) → Context API +
useReducer - Medium (multi-feature, moderate state) → Zustand or Jotai
- Complex (many features, API caching, middleware) → Redux Toolkit + RTK Query Checkpoint: If you need API caching, choose RTK Query or React Query before adding a state library.
- Simple (1-2 shared values) → Context API +
Define State Shape — Design flat, normalized state slices:
- Keep state as flat as possible — avoid deeply nested objects
- Never store derived data (filtered lists, computed totals) — compute with selectors
- Separate server state (API data) from client state (UI toggles, form inputs) Checkpoint: Can every piece of state be serialized to JSON? If not, move non-serializable data to a ref or middleware.
Implement Store with Selectors — Create the store/slice with memoized selectors:
- Use
createSelector(Redux) oruseShallow(Zustand) for equality checks - Code-split by feature — one store/slice per app domain
- Keep actions and reducers colocated with their feature Checkpoint: Verify that components only re-render when their specific slice of state changes.
- Use
Add Persistence — Wire AsyncStorage with the appropriate middleware:
- Redux:
redux-persistwithAsyncStorageengine - Zustand:
persistmiddleware - Jotai:
atomWithStoragefromjotai/utils - Only persist essential user data (auth tokens, preferences, drafts) — never cache full API responses Checkpoint: Test that state survives app kill/restart on both iOS and Android.
- Redux:
Handle Auth Flow — Implement login/logout with proper state lifecycle:
- On login: restore persisted state or initialize fresh
- On logout: dispatch a single
RESET_STATEaction or callstore.reset() - Never manually clear individual slices — one atomic reset prevents stale data bugs Checkpoint: Verify that after logout, no previous user's data is accessible.
Implementation Patterns
Pattern 1: Zustand Store with Persistence (Medium Complexity)
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { useShallow } from 'zustand/react/shallow';
// --- State shape ---
interface UserPreferences {
theme: 'light' | 'dark' | 'system';
fontSize: number;
notificationsEnabled: boolean;
}
interface AppState {
// Auth
userId: string | null;
token: string | null;
// Preferences (persisted)
preferences: UserPreferences;
// Actions
login: (userId: string, token: string) => void;
logout: () => void;
updatePreferences: (prefs: Partial<UserPreferences>) => void;
}
const initialState = {
userId: null,
token: null,
preferences: {
theme: 'system' as const,
fontSize: 16,
notificationsEnabled: true,
},
};
// --- Zustand store with persistence ---
export const useAppStore = create<AppState>()(
persist(
(set) => ({
...initialState,
login: (userId, token) => set({ userId, token }),
logout: () => set(initialState), // Single atomic reset
updatePreferences: (prefs) =>
set((state) => ({
preferences: { ...state.preferences, ...prefs },
})),
}),
{
name: 'app-state',
storage: createJSONStorage(() => AsyncStorage),
// Only persist auth tokens and preferences — skip transient UI state
partialize: (state) => ({
userId: state.userId,
token: state.token,
preferences: state.preferences,
}),
}
)
);
// --- Selector with shallow comparison prevents unnecessary re-renders ---
export function useTheme() {
return useAppStore(useShallow((state) => ({
theme: state.preferences.theme,
fontSize: state.preferences.fontSize,
})));
}
Pattern 2: Redux Toolkit with RTK Query (Complex App)
import { configureStore, createSlice, createSelector, PayloadAction } from '@reduxjs/toolkit';
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
import {
persistStore,
persistReducer,
FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER,
} from 'redux-persist';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
// --- API slice (server state) ---
interface Post {
id: number;
title: string;
body: string;
}
export const postsApi = createApi({
reducerPath: 'postsApi',
baseQuery: fetchBaseQuery({ baseUrl: 'https://jsonplaceholder.typicode.com' }),
endpoints: (builder) => ({
getPosts: builder.query<Post[], void>({
query: () => '/posts',
}),
getPost: builder.query<Post, number>({
query: (id) => `/posts/${id}`,
}),
}),
});
export const { useGetPostsQuery, useGetPostQuery } = postsApi;
// --- Client state slice (UI-only state) ---
interface UIState {
selectedPostId: number | null;
isGridView: boolean;
}
const uiSlice = createSlice({
name: 'ui',
initialState: {
selectedPostId: null,
isGridView: false,
} as UIState,
reducers: {
selectPost(state, action: PayloadAction<number | null>) {
state.selectedPostId = action.payload;
},
toggleView(state) {
state.isGridView = !state.isGridView;
},
},
});
export const { selectPost, toggleView } = uiSlice.actions;
// --- Memoized selector ---
const selectIsGridView = (state: RootState) => state.ui.isGridView;
export const selectLayoutMode = createSelector(
[selectIsGridView],
(isGridView) => (isGridView ? 'grid' : 'list' as const)
);
// --- Persist only UI slice (RTK Query manages its own cache) ---
const persistConfig = {
key: 'ui',
storage: AsyncStorage,
whitelist: ['isGridView'], // Don't persist selectedPostId — it's transient
};
const persistedUIReducer = persistReducer(persistConfig, uiSlice.reducer);
// --- Store ---
export const store = configureStore({
reducer: {
[postsApi.reducerPath]: postsApi.reducer,
ui: persistedUIReducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
},
}).concat(postsApi.middleware),
});
export const persistor = persistStore(store);
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
Pattern 3: Jotai for Fine-Grained Atomic State
import { atom, useAtom, useAtomValue } from 'jotai';
import { atomWithStorage, splitAtom } from 'jotai/utils';
import AsyncStorage from '@react-native-async-storage/async-storage';
// --- Persisted atoms ---
export const themeAtom = atomWithStorage<'light' | 'dark'>(
'theme',
'light',
{
getItem: async (key) => {
const value = await AsyncStorage.getItem(key);
return value === 'dark' ? 'dark' : 'light';
},
setItem: async (key, value) => {
await AsyncStorage.setItem(key, value);
},
removeItem: async (key) => {
await AsyncStorage.removeItem(key);
},
}
);
// --- Derived atom (no derived data stored in state) ---
export const isDarkAtom = atom((get) => get(themeAtom) === 'dark');
// --- Atom family (splitAtom for dynamic lists) ---
interface TodoItem {
id: string;
text: string;
completed: boolean;
}
export const todosAtom = atom<TodoItem[]>([]);
export const todoAtomsAtom = splitAtom(todosAtom);
// --- Computed via selectors, not stored ---
export const completedTodosAtom = atom((get) =>
get(todosAtom).filter((todo) => todo.completed)
);
export const pendingTodosAtom = atom((get) =>
get(todosAtom).filter((todo) => !todo.completed)
);
// --- Component usage (only re-renders when specific atom changes) ---
function ThemeToggle() {
const [theme, setTheme] = useAtom(themeAtom);
const isDark = useAtomValue(isDarkAtom);
return (
<button => setTheme(theme === 'light' ? 'dark' : 'light')}>
Current: {theme} {isDark ? '🌙' : '☀️'}
</button>
);
}
Pattern 4: Context API Pitfall — When Not to Use It
// ❌ BAD: Context for rapidly-updating global state (causes full-tree re-renders)
const PositionContext = createContext({
latitude: 0,
longitude: 0,
heading: 0,
speed: 0,
});
function PositionProvider({ children }: { children: React.ReactNode }) {
const [position, setPosition] = useState({ latitude: 0, longitude: 0, heading: 0, speed: 0 });
useEffect(() => {
const sub = Location.watchPositionAsync(
{ enableHighAccuracy: true, timeInterval: 100 },
(loc) => setPosition({
latitude: loc.coords.latitude,
longitude: loc.coords.longitude,
heading: loc.coords.heading ?? 0,
speed: loc.coords.speed ?? 0,
})
);
return () => sub.then((s) => s.remove());
}, []);
return <PositionContext.Provider value={position}>{children}</PositionContext.Provider>;
}
// Every consumer re-renders 10 times/second — even those that only show heading!
// ✅ GOOD: Use Zustand for high-frequency updates (only subscribed components re-render)
import { create } from 'zustand';
interface PositionState {
latitude: number;
longitude: number;
heading: number;
speed: number;
setPosition: (pos: Omit<PositionState, 'setPosition'>) => void;
}
export const usePositionStore = create<PositionState>((set) => ({
latitude: 0,
longitude: 0,
heading: 0,
speed: 0,
setPosition: (pos) => set(pos),
}));
// Only this component re-renders on each GPS update
function HeadingDisplay() {
const heading = usePositionStore((s) => s.heading); // Subscribes only to `heading`
return <Text>Heading: {heading}°</Text>;
}
Constraints
MUST DO
- Use selectors with equality checks (useShallow, shallow, createSelector) to prevent unnecessary re-renders
- Implement state persistence via AsyncStorage for user preferences, auth tokens, and draft data
- Handle auth flow state reset atomically — one action clears everything
- Code-split stores by feature domain (auth/, feed/, settings/) in large apps
- Keep state serializable at all times — non-serializable data belongs in refs or middleware
- Store only the minimum necessary state — derive everything else via selectors
MUST NOT DO
- Store derived/computed data in state — compute with selectors instead
- Use Context API for high-frequency global state updates (GPS, websocket data, animation values)
- Store full API responses in persisted state — use RTK Query cache or React Query instead
- Mix server state (API data) and client state (UI toggles) in the same slice
- Reset state by manually clearing individual keys — always use an atomic reset action
- Use mutable patterns in Redux Toolkit (the immer-based reducer handles immutability for you)
Related Skills
| Skill | Purpose |
|---|---|
react-native-rendering |
Optimizes component rendering in tandem with state management |
react-native-monorepo-config |
Configures monorepo workspaces for sharing state libraries across packages |
Live References
Authoritative documentation links for React Native state management.
- React Native State Management Overview — Official React Native guide
- Zustand Documentation — Lightweight state management for React
- Redux Toolkit Docs — Official Redux Toolkit with RTK Query
- Jotai Documentation — Atomic state management for React
- Redux Persist — Persist and rehydrate Redux state
- AsyncStorage Docs — Persistent key-value storage for React Native
- React Native Performance: State Management — Performance implications of state management choices