# React Native State Management

> Manages application state in React Native using Context API, Zustand, Redux Toolkit, and Jotai with proper persistence, middleware, and re-render optimization for mobile.

- Skill: `paulpas/react-native-state-management` (Agent Skill)
- Install (CLI): `npx skillmds@latest add paulpas/react-native-state-management`
- Raw SKILL.md: https://api.skillmd.com/api/skills/paulpas/react-native-state-management/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- License: MIT
- Author: paulpas (https://skillmd.com/u/paulpas)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/paulpas/react-native-state-management

---


# 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

1. **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.

2. **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.

3. **Implement Store with Selectors** — Create the store/slice with memoized selectors:
   - Use `createSelector` (Redux) or `useShallow` (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.

4. **Add Persistence** — Wire AsyncStorage with the appropriate middleware:
   - Redux: `redux-persist` with `AsyncStorage` engine
   - Zustand: `persist` middleware
   - Jotai: `atomWithStorage` from `jotai/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.

5. **Handle Auth Flow** — Implement login/logout with proper state lifecycle:
   - On login: restore persisted state or initialize fresh
   - On logout: dispatch a single `RESET_STATE` action or call `store.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)

```tsx
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)

```tsx
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

```tsx
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 onPress={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
      Current: {theme} {isDark ? '🌙' : '☀️'}
    </button>
  );
}
```

### Pattern 4: Context API Pitfall — When Not to Use It

```tsx
// ❌ 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](https://reactnative.dev/docs/state) — Official React Native guide
- [Zustand Documentation](https://github.com/pmndrs/zustand) — Lightweight state management for React
- [Redux Toolkit Docs](https://redux-toolkit.js.org/) — Official Redux Toolkit with RTK Query
- [Jotai Documentation](https://jotai.org/docs) — Atomic state management for React
- [Redux Persist](https://github.com/rt2zz/redux-persist) — Persist and rehydrate Redux state
- [AsyncStorage Docs](https://react-native-async-storage.github.io/async-storage/) — Persistent key-value storage for React Native
- [React Native Performance: State Management](https://reactnative.dev/docs/performance#state-management) — Performance implications of state management choices

