> ## Documentation Index
> Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 11. Redux & Zustand

> Global state management

## Learning Objectives

By the end of this module, you'll understand:

* When to choose Redux Toolkit vs. Zustand (and why Zustand often wins for mobile)
* Redux Toolkit setup with typed slices
* Zustand for lightweight, performant state
* State persistence patterns for mobile apps
* Common pitfalls with global state on React Native

***

## Choosing a State Library

The React Native ecosystem offers many global state libraries, but two dominate production apps: **Redux Toolkit** and **Zustand**. Think of them as a pickup truck vs. a sports car -- both get you there, but they are optimized for different jobs.

| Factor             | Redux Toolkit                          | Zustand                             |
| ------------------ | -------------------------------------- | ----------------------------------- |
| **Boilerplate**    | Moderate (slices, store, Provider)     | Minimal (single `create` call)      |
| **Bundle size**    | \~11 KB (RTK) + \~5 KB (react-redux)   | \~1 KB                              |
| **DevTools**       | Excellent (Redux DevTools)             | Good (via middleware)               |
| **Middleware**     | Built-in (thunks, listeners)           | Plugin-based                        |
| **Learning curve** | Steeper (actions, reducers, selectors) | Gentle (just functions and objects) |
| **Best for**       | Large teams, complex business logic    | Most mobile apps, rapid development |

**The practical recommendation:** Start with Zustand for new React Native projects. Its tiny footprint matters on mobile, and its API is simpler to onboard teammates with. Reach for Redux Toolkit when you need its middleware ecosystem (RTK Query, listener middleware) or when your team already has Redux expertise.

***

## Redux Toolkit

### Installation

```bash theme={null}
npm install @reduxjs/toolkit react-redux
```

### Store Configuration

```tsx theme={null}
// src/stores/store.ts
import { configureStore } from '@reduxjs/toolkit';
import { authReducer } from './slices/authSlice';
import { projectReducer } from './slices/projectSlice';

export const store = configureStore({
  reducer: {
    auth: authReducer,
    projects: projectReducer,
  },
  // Redux Toolkit enables Redux DevTools and thunk middleware by default.
  // On React Native, the Flipper Redux plugin connects automatically.
});

// Infer types from the store itself so they stay in sync with your slices.
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
```

### Typed Slice Example

```tsx theme={null}
// src/stores/slices/authSlice.ts
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';

interface User {
  id: string;
  name: string;
  email: string;
}

interface AuthState {
  user: User | null;
  token: string | null;
  status: 'idle' | 'loading' | 'succeeded' | 'failed';
  error: string | null;
}

const initialState: AuthState = {
  user: null,
  token: null,
  status: 'idle',
  error: null,
};

// createAsyncThunk handles the pending/fulfilled/rejected lifecycle for you.
// This replaces the manual dispatch({ type: 'FETCH_START' }) pattern.
export const login = createAsyncThunk(
  'auth/login',
  async (credentials: { email: string; password: string }) => {
    const response = await fetch('/api/login', {
      method: 'POST',
      body: JSON.stringify(credentials),
    });
    if (!response.ok) throw new Error('Invalid credentials');
    return response.json(); // { user, token }
  }
);

const authSlice = createSlice({
  name: 'auth',
  initialState,
  reducers: {
    // Synchronous actions -- RTK uses Immer internally, so you can
    // "mutate" state directly. It produces an immutable update behind the scenes.
    logout(state) {
      state.user = null;
      state.token = null;
      state.status = 'idle';
    },
    updateProfile(state, action: PayloadAction<Partial<User>>) {
      if (state.user) {
        Object.assign(state.user, action.payload);
      }
    },
  },
  extraReducers: (builder) => {
    builder
      .addCase(login.pending, (state) => {
        state.status = 'loading';
        state.error = null;
      })
      .addCase(login.fulfilled, (state, action) => {
        state.status = 'succeeded';
        state.user = action.payload.user;
        state.token = action.payload.token;
      })
      .addCase(login.rejected, (state, action) => {
        state.status = 'failed';
        state.error = action.error.message ?? 'Login failed';
      });
  },
});

export const { logout, updateProfile } = authSlice.actions;
export const authReducer = authSlice.reducer;
```

### Typed Hooks

```tsx theme={null}
// src/stores/hooks.ts
import { useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux';
import type { RootState, AppDispatch } from './store';

// Always use these typed hooks instead of plain useDispatch/useSelector.
// They give you autocomplete on state shape and dispatch types.
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
```

<Warning>
  **Mobile pitfall with Redux:** Every dispatched action triggers the root reducer, and `useSelector` runs on every store update. In a busy mobile app with real-time data, this can cause performance issues. Always use specific selectors that return the narrowest slice of state possible, and use `createSelector` from RTK for memoized derived data.
</Warning>

***

## Zustand

### Installation

```bash theme={null}
npm install zustand
```

### Basic Store

Zustand stores are plain functions -- no providers, no reducers, no action types. You call `create`, define your state and actions in one place, and consume it with a hook.

```tsx theme={null}
// src/stores/useCounterStore.ts
import { create } from 'zustand';

interface CounterStore {
  count: number;
  increment: () => void;
  decrement: () => void;
  reset: () => void;
}

export const useCounterStore = create<CounterStore>((set) => ({
  count: 0,
  // set() merges the partial state update, just like React's setState.
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
  reset: () => set({ count: 0 }),
}));
```

### Real-World Store with Async Actions

```tsx theme={null}
// src/stores/useProjectStore.ts
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import AsyncStorage from '@react-native-async-storage/async-storage';

interface Project {
  id: string;
  name: string;
  description: string;
}

interface ProjectStore {
  projects: Project[];
  isLoading: boolean;
  error: string | null;

  fetchProjects: () => Promise<void>;
  addProject: (project: Project) => void;
  removeProject: (id: string) => void;
}

export const useProjectStore = create<ProjectStore>()(
  // The persist middleware automatically saves and restores state to AsyncStorage,
  // giving your app offline resilience with zero extra effort.
  persist(
    (set, get) => ({
      projects: [],
      isLoading: false,
      error: null,

      fetchProjects: async () => {
        set({ isLoading: true, error: null });
        try {
          const response = await fetch('/api/projects');
          const projects = await response.json();
          set({ projects, isLoading: false });
        } catch (err) {
          set({ error: (err as Error).message, isLoading: false });
        }
      },

      addProject: (project) =>
        set((state) => ({ projects: [...state.projects, project] })),

      removeProject: (id) =>
        set((state) => ({
          projects: state.projects.filter((p) => p.id !== id),
        })),
    }),
    {
      name: 'project-storage',
      storage: createJSONStorage(() => AsyncStorage),
      // Only persist the project list, not loading/error transient state
      partialize: (state) => ({ projects: state.projects }),
    }
  )
);
```

### Selective Subscriptions (Performance)

One of Zustand's biggest advantages on mobile is that components only re-render when the specific value they subscribe to changes.

```tsx theme={null}
function ProjectCount() {
  // This component ONLY re-renders when the length of projects changes,
  // not when isLoading or error changes. No memoized selectors needed.
  const count = useProjectStore((state) => state.projects.length);
  return <Text>{count} projects</Text>;
}

function ProjectList() {
  // This subscribes to the full projects array
  const projects = useProjectStore((state) => state.projects);
  const fetchProjects = useProjectStore((state) => state.fetchProjects);

  useEffect(() => {
    fetchProjects();
  }, []);

  return (
    <FlatList
      data={projects}
      keyExtractor={(item) => item.id}
      renderItem={({ item }) => <Text>{item.name}</Text>}
    />
  );
}
```

<Tip>
  **Mobile tip:** For MMKV-based persistence (faster than AsyncStorage by 30x), replace `createJSONStorage(() => AsyncStorage)` with a custom MMKV storage adapter. This makes a noticeable difference on app startup when restoring large persisted stores.
</Tip>

***

## Best Practices

1. **One store per domain, not one giant store** -- Create separate Zustand stores for auth, projects, UI preferences, etc. This keeps each store focused and prevents unrelated re-renders.
2. **Never put server data in global state** -- Use React Query for data from your API. Global stores are for *client state* (UI preferences, auth tokens, local-only data).
3. **Persist selectively** -- Only persist what the user expects to survive an app restart. Transient state like loading flags and error messages should not be persisted.
4. **Use the functional updater** -- Always use `set((state) => ...)` instead of `set({ ... })` when the new state depends on the previous state. This avoids race conditions from rapid state updates.
