State Management: Redux & Zustand

The Context API solves prop drilling well for state that rarely changes, but it wasn't designed to be a complete state-management system — no fine-grained subscriptions, no middleware, no dedicated developer tools. When an app's shared state grows large, changes often, or is touched by many independent parts of the UI, most React Native teams reach for a dedicated library. This lesson covers the two most popular: Redux Toolkit, the modern, official way to use Redux, and Zustand, a newer, much lighter-weight alternative that has rapidly grown in popularity.

Redux Toolkit: A Predictable, Centralized Store

Classic Redux (the original library, from years ago) required a lot of hand-written boilerplate — action type constants, action creator functions, and switch-statement reducers, often duplicated across features. Redux Toolkit (commonly imported as @reduxjs/toolkit) is the officially recommended way to write Redux today, and it eliminates almost all of that boilerplate while keeping Redux's core idea intact.

That core idea is simple to state: your entire app's shared state lives in one single store, and the only way to change anything in that store is by dispatching an action — a plain object describing "what happened." Components never mutate the store directly; they dispatch an action, and a reducer function decides how the store should change in response. This one-way flow is what makes Redux apps predictable, even as they grow large: every state change can be traced back to a specific, named action.

A Slice: Bundling State and Reducers Together

Redux Toolkit's createSlice bundles a piece of state, its initial value, and the reducer functions that can change it, all in one place — no separate action-type constants or switch statements required:

import { createSlice } from '@reduxjs/toolkit';

const cartSlice = createSlice({
  name: 'cart',
  initialState: [],
  reducers: {
    addItem: (state, action) => {
      // Redux Toolkit uses Immer under the hood, so "mutating"
      // state here is actually safe and produces an immutable update
      state.push(action.payload);
    },
    removeItem: (state, action) => {
      return state.filter(item => item.id !== action.payload);
    },
  },
});

export const { addItem, removeItem } = cartSlice.actions;
export default cartSlice.reducer;

The store itself is assembled from one or more slice reducers with configureStore, usually once, near the root of the app:

import { configureStore } from '@reduxjs/toolkit';
import cartReducer from './cartSlice';

export const store = configureStore({
  reducer: {
    cart: cartReducer,
  },
});

And the whole app is wrapped once in a Provider from react-redux, connecting the store to the component tree — conceptually similar to the Context Provider from the previous lesson, but purpose-built for Redux:

import { Provider } from 'react-redux';
import { store } from './store';

function App() {
  return (
    <Provider store={store}>
      <RootNavigator />
    </Provider>
  );
}

Connecting a Component: useSelector and useDispatch

Inside any component, useSelector reads a piece of state from the store, and useDispatch gives you the function used to dispatch actions:

import React from 'react';
import { View, Text, Button } from 'react-native';
import { useSelector, useDispatch } from 'react-redux';
import { addItem } from './cartSlice';

function ProductScreen() {
  const cartItems = useSelector(state => state.cart);
  const dispatch = useDispatch();

  return (
    <View>
      <Text>Cart has {cartItems.length} item(s)</Text>
      <Button
        title="Add running shoes"
        onPress={() => dispatch(addItem({ id: 'p_1', name: 'Running Shoes' }))}
      />
    </View>
  );
}

useSelector only re-renders this component when the specific slice it reads ( state.cart) actually changes — a more precise, fine-grained subscription than a plain Context consumer gets, which is part of why Redux scales better to large, frequently updated state.

Zustand: A Much Lighter Alternative

Zustand ("state" in German) takes almost the opposite approach: no slices, no reducers, no Provider wrapping your app, no action objects to dispatch. A single create() call defines your state and the functions that update it, together, in one place:

import { create } from 'zustand';

const useCartStore = create((set) => ({
  items: [],
  addItem: (product) =>
    set((state) => ({ items: [...state.items, product] })),
  removeItem: (id) =>
    set((state) => ({ items: state.items.filter(item => item.id !== id) })),
}));

That single useCartStore value is a custom hook — no Provider needed anywhere. Any component simply imports it and calls it directly, picking out just the piece of state or the action function it needs:

import React from 'react';
import { View, Text, Button } from 'react-native';
import { useCartStore } from './cartStore';

function ProductScreen() {
  const items = useCartStore(state => state.items);
  const addItem = useCartStore(state => state.addItem);

  return (
    <View>
      <Text>Cart has {items.length} item(s)</Text>
      <Button
        title="Add running shoes"
        onPress={() => addItem({ id: 'p_1', name: 'Running Shoes' })}
      />
    </View>
  );
}

Compare this directly to the Redux Toolkit version above: same cart feature, same addItem behavior — but no slice file, no configureStore, no Provider, and no dispatched action objects. This dramatically smaller footprint is exactly why Zustand has become so popular for small-to-medium apps and teams who want shared state without Redux's ceremony.

Decision Guide: Which Approach Should You Use?

Between local useState, Context, Zustand, and Redux Toolkit, there's no single "correct" answer — each is the right tool for a different situation:

ApproachBest for
useStateSimple state that lives inside a single component and doesn't need to be shared
Context APISimple state shared across many components, that changes infrequently — theme, auth, language
ZustandLightweight shared state with minimal boilerplate; a fast-growing favorite for small-to-medium apps and state that updates often
Redux ToolkitThe most structured, tooling-rich option — Redux DevTools, middleware, strict conventions — often favored on larger teams and apps with genuinely complex, interconnected state
A practical rule of thumb: start with useState. Reach for Context when multiple distant components need the same rarely-changing value. Reach for Zustand when you need real shared state that updates often but want to avoid Redux's setup. Reach for Redux Toolkit when the app (or team) is large enough that dedicated dev tools, middleware, and strict conventions genuinely pay for themselves.
Next up: With state management covered, the next lesson turns to real-world data: Networking with Fetch & Axios — how to actually load data from an API into the state you now know how to manage.