State management: Zustand vs Redux Toolkit compared
AI generated
RN
native
React Native · State Management · Zustand · Redux Toolkit
State management: Zustand vs Redux Toolkit compared
Boilerplate, performance and persistence in React Native apps

The choice of state management approach in React Native apps determines boilerplate volume, re-render behavior and maintainability. Zustand and Redux Toolkit solve the same underlying problem with a different philosophy: a minimal, hooks based store versus a structured, battery included Redux ecosystem with DevTools, RTK Query and mature tooling.

18 min read Zustand · Redux Toolkit · RTK Query · Persistence · DevTools React Native 0.74+ · Zustand 4.x/5.x · Redux Toolkit 2.x

1. Why the state management approach matters

Classic Redux demanded action types, action creators, a reducer and manual dispatch wiring for every state change, before a single line of business logic was even written. Redux Toolkit cut that boilerplate drastically with createSlice and Immer based reducers, but it still remains structurally heavier than a minimal hooks store. For state management in React Native projects with a tight schedule, that difference adds up quickly across dozens of slices and screens.

On mobile devices, unnecessary re-renders cost noticeably more than in the browser: every update passes through the bridge, or the newer Fabric architecture, before it reaches native views, blocking the JS thread along the way. A state management approach without granular subscriptions re-renders entire component trees whenever any part of the global state changes, which shows up as visible frame drops during scrolling or gesture interactions. This is exactly where Zustand and Redux Toolkit diverge in practice, even though both technically offer selective subscriptions.

The two dominant answers to this question today are Zustand, a minimalist, hooks based store with no provider requirement, and Redux Toolkit, the official, battery included evolution of Redux with RTK Query, entity adapters and mature DevTools. Both solve state management for React Native apps reliably, but they differ significantly in boilerplate, learning curve and ecosystem depth, which the following sections cover in detail.

2. Zustand: setup and core principle

Zustand reduces state management to a single function: create() produces a hook that exposes both the current state and the actions used to change it. There is no mandatory provider, no context nesting and no separation between action type constants and reducer functions. A store is a plain JavaScript object with set and get, given access to itself through closure.

Installation is minimal, a single package with no peer dependencies tied to React context internals. Components subscribe to a Zustand store through the same hook that created it, with a selector function passed as an argument to determine which slice of the state the component actually cares about. When some other part of the store changes, the component does not re-render, because Zustand only checks the returned value by reference internally.

This core principle makes Zustand particularly attractive for small to medium sized React Native apps, where the ceremony overhead of Redux Toolkit is out of proportion to the actual state complexity. Multiple independent stores can be created in parallel, for example an auth store and a cart store, without a central root reducer having to merge them.


# Install Zustand: single package, no extra peer dependencies
npm install zustand

# Optional: persist middleware needs AsyncStorage as the storage adapter
npm install @react-native-async-storage/async-storage

// store/useCartStore.js
import { create } from 'zustand';

// A Zustand store is just a hook created by create()
export const useCartStore = create((set, get) => ({
  items: [],
  addItem: (product) =>
    set((state) => ({ items: [...state.items, product] })),
  removeItem: (id) =>
    set((state) => ({ items: state.items.filter((i) => i.id !== id) })),
  clear: () => set({ items: [] }),
  // Derived value computed from current state via get()
  total: () => get().items.reduce((sum, i) => sum + i.price, 0),
}));

// Component: subscribe only to the slice this component actually needs
function CartBadge() {
  const itemCount = useCartStore((state) => state.items.length);
  return <Text>Cart: {itemCount}</Text>;
}

3. Redux Toolkit: setup and core principle

Redux Toolkit builds on classic Redux, but wraps the three previously separate concepts, action types, action creators and reducers, into a single createSlice definition. Internally, createSlice uses Immer, which lets reducer functions write code that looks like it mutates state, while a new, immutable state is still produced behind the scenes. That significantly lowers the error rate compared to hand written Redux, without abandoning the core principle of Redux itself.

The store itself is created through configureStore, which automatically enables sensible defaults such as the Redux Thunk middleware and Redux DevTools integration. Unlike Zustand, every Redux Toolkit app needs a Provider wrapper around the root component, which passes the store down through context to the entire component hierarchy. This centralized structure enforces a single state tree, which helps keep large teams with many parallel feature branches consistent.

For state management in large React Native apps with several domains, for example auth, catalog, cart and checkout, Redux Toolkit brings structure through entity adapters, selectors and a clear set of conventions that would otherwise have to be rebuilt by hand with a purely hooks based approach.


// store/cartSlice.js
import { createSlice, configureStore } from '@reduxjs/toolkit';

const cartSlice = createSlice({
  name: 'cart',
  initialState: { items: [] },
  reducers: {
    // Immer lets us write "mutating" code, it produces an immutable update under the hood
    addItem: (state, action) => {
      state.items.push(action.payload);
    },
    removeItem: (state, action) => {
      state.items = state.items.filter((i) => i.id !== action.payload);
    },
    clear: (state) => {
      state.items = [];
    },
  },
});

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

// configureStore enables Redux DevTools and thunk middleware by default
export const store = configureStore({
  reducer: { cart: cartSlice.reducer },
});

// App root: Provider makes the store available via React context
// <Provider store={store}><App /></Provider>

4. Asynchronous actions and side effects

Zustand has no dedicated mechanism for asynchronous actions, because none is needed: a store action can simply be an async function that awaits a promise before the final set() call. Intermediate states like loading or error are tracked as plain state fields, set through additional set() calls before and after the await. For simple API calls, this makes state management about as direct as it gets, with no middleware and no extra concepts.

Redux Toolkit offers a structured pattern with createAsyncThunk: the thunk wraps the async call, and three automatically generated action types, pending, fulfilled and rejected, can be handled explicitly in extraReducers. For data heavy apps, RTK Query goes a step further and takes over caching, deduplication of parallel requests and automatic invalidation after mutations, so loading flags no longer have to be managed by hand.

The practical difference shows up at scale: a single API call takes three lines with Zustand, while Redux Toolkit starts with an extra thunk setup. But once dozens of endpoints come together with caching, refetching on reconnect and optimistic updates, the balance flips, because RTK Query has already thought through exactly those cases.

5. Persisting state across app restarts

For Zustand, the persist middleware from zustand/middleware is enough, connecting the store to a storage adapter at creation time. In React Native, @react-native-async-storage/async-storage plays that role, since localStorage does not exist on the device. With partialize, it is possible to specify exactly which fields are actually persisted, for example auth tokens but not transient UI flags.

Redux Toolkit classically relies on redux-persist for persistence: the root reducer is wrapped with persistReducer, a persistStore object is created separately, and the app root is wrapped in a PersistGate component that delays rendering until the saved state has loaded. This setup is more involved than Zustand's, but offers granular configuration through whitelists and blacklists per slice.

Both approaches solve state management across app restarts reliably, but differ in ceremony: Zustand needs a single middleware function, Redux Toolkit needs an extra provider and a wrapper component. For apps with many slices and different persistence strategies per domain, redux-persist offers more built in configuration options.


{
  "persistConfig": {
    "name": "app-storage",
    "storage": "AsyncStorage",
    "partialize": ["auth", "cartItems"],
    "version": 1
  },
  "dependencies": {
    "zustand": "^5.0.0",
    "@react-native-async-storage/async-storage": "^1.23.1",
    "@reduxjs/toolkit": "^2.2.0",
    "redux-persist": "^6.0.0",
    "react-redux": "^9.1.0"
  }
}

6. DevTools and debugging for both approaches

For Redux Toolkit, the connection to Redux DevTools is active by default through configureStore, with no extra configuration needed. Time travel debugging, action replay and a complete history of every dispatched action are part of the standard scope, and redux-flipper shows the same action log directly inside Flipper within the React Native development environment.

Zustand offers a connection to the same Redux DevTools protocol through the devtools middleware from zustand/middleware, so Zustand state changes also show up in the familiar DevTools panel. The middleware is optional, though, and must be explicitly enabled per store, whereas it applies implicitly to the entire store in Redux Toolkit. Flipper integration for Zustand is less established than for Redux Toolkit and usually requires its own small bridge configuration.

7. Performance and re-renders

With Zustand, the selector function passed to the hook call already prevents unnecessary re-renders in the basic case, since only the returned value is checked by reference. When several fields are selected at once, for example { name, email }, a new object is created on every call, which makes the reference comparison fail every time. The fix is zustand/shallow as a second argument, which compares fields shallowly instead of by reference identity.

Redux Toolkit addresses the same problem with memoized selectors using createSelector from Reselect, which is included automatically in the Toolkit. For lists with many entries, createEntityAdapter additionally prevents a small change to a single entry from re-running the entire array reducer, because the state is kept internally as a normalized dictionary keyed by id instead of an array.

In both worlds, the same basic rule applies to state management on mobile devices: a useStore() or useSelector() call without a selector function subscribes to the entire store and turns even the smallest change into a trigger for a component re-render. Disciplined selector design is the single most important lever for smooth scroll and list performance in either library.

8. Testing stores and slices

A Zustand store is, at its core, a plain JavaScript object with getState() and setState(), which means tests need no provider mocking and no component rendering at all. Before each test, the store can simply be reset with useCartStore.setState(initialState, true), where the second parameter replaces the entire state instead of merging into it.

A Redux Toolkit slice reducer is a pure function that takes a state and an action and returns a new state, which means unit tests need no store setup at all, just plain action objects. For integration tests that check several slices together, a configureStore instance is created with the reducers under test.

Both approaches make state management highly testable, because the actual logic is decoupled from the UI in either case. The difference lies in the scope of test infrastructure: Zustand tests need almost no setup, while Redux Toolkit tests benefit from the clear separation between pure reducers and thunk logic, which can be mocked separately.


// __tests__/cartStore.test.js
import { useCartStore } from '../store/useCartStore';

describe('cart store', () => {
  beforeEach(() => {
    // Replace full state before every test, no Provider needed
    useCartStore.setState({ items: [] }, true);
  });

  test('addItem appends a product', () => {
    useCartStore.getState().addItem({ id: 1, price: 9.99 });
    expect(useCartStore.getState().items).toHaveLength(1);
  });

  test('total sums item prices', () => {
    useCartStore.getState().addItem({ id: 1, price: 9.99 });
    useCartStore.getState().addItem({ id: 2, price: 5.0 });
    expect(useCartStore.getState().total()).toBeCloseTo(14.99);
  });
});

9. Zustand vs Redux Toolkit head to head

The table below summarizes the key differences between Zustand and Redux Toolkit for state management in React Native apps. Neither library is fundamentally superior, the right choice depends on project size, team conventions and the need for built in extras like RTK Query.

Criterion Zustand Redux Toolkit
Boilerplate Minimal, a single create() call per store Low thanks to createSlice, but more structure required
Bundle size Very small, a few KB gzipped Larger due to the Redux core, Toolkit and Immer
Learning curve Flat, essentially just hooks knowledge needed Steeper, Redux concepts like reducers and actions required
DevTools Optional via middleware, less mature Active by default, time travel debugging
Async handling Plain async functions inside the store createAsyncThunk or RTK Query with caching
Persistence zustand/middleware persist, little setup redux-persist, more configuration, more control

In practice, team experience often decides: teams with a Redux background benefit from the structured framework of Redux Toolkit and its ecosystem, while teams that want to keep state management as lean as possible become productive faster with Zustand. Both libraries can also be used in parallel within the same app, for example Zustand for local UI state and Redux Toolkit for server synchronized domain data through RTK Query.

10. Summary

State management in React Native apps is not purely a matter of taste, it has a direct effect on bundle size, re-render behavior and development speed. Zustand scores with minimal boilerplate, a flat learning curve and a store definition that needs neither a provider nor Redux vocabulary. Redux Toolkit brings a mature ecosystem instead, DevTools active by default, RTK Query for server data, and entity adapters for normalized lists.

For new, manageable apps, Zustand is often the more pragmatic entry point into state management, because the ceremony overhead of Redux Toolkit only pays off once complexity grows. Large teams with many parallel feature branches, complex server state and a need for time travel debugging, on the other hand, are often better served by the structure and maturity of Redux Toolkit. The table in section 9 provides the decision basis for the concrete project situation.

Zustand vs Redux Toolkit: the essentials at a glance

Boilerplate

Zustand needs just a single create() call, Redux Toolkit structures state through createSlice and configureStore.

Async & persistence

Zustand uses plain async functions and the persist middleware. Redux Toolkit relies on createAsyncThunk, RTK Query and redux-persist.

DevTools

Redux Toolkit enables Redux DevTools by default. Zustand needs the optional devtools middleware for the same feature.

Performance

Both need disciplined selector design: zustand/shallow for Zustand, createSelector and entity adapters for Redux Toolkit.

11. FAQ: Zustand vs Redux Toolkit

1Biggest difference between Zustand and Redux Toolkit?
Zustand reduces state management to a create() call with no provider. Redux Toolkit structures it via createSlice and configureStore, with default DevTools and RTK Query.
2Does Zustand need a provider?
No, a Zustand store is a self contained hook. Redux Toolkit requires a provider around the app root.
3How does Zustand handle async actions?
A store action can be an async function that awaits a promise before set(). No dedicated middleware needed.
4What does RTK Query add on top?
Caching, deduplication of parallel requests and automatic invalidation after mutations, with no manual loading flags.
5Persistence with Zustand in React Native?
persist middleware from zustand/middleware, connected to AsyncStorage. partialize defines which fields get saved.
6Persistence with redux-persist?
persistReducer wraps the root reducer, persistStore creates the store, PersistGate delays rendering until loading completes.
7DevTools support for both?
Redux Toolkit enables DevTools by default. Zustand offers the same via the optional devtools middleware.
8How to avoid unnecessary re-renders?
With Zustand via selectors and zustand/shallow. With Redux Toolkit via createSelector and createEntityAdapter for normalized lists.
9How to test a store or slice?
Reset a Zustand store with setState() before each test. A Redux Toolkit reducer is a pure function, testable directly with action objects.
10Zustand or Redux Toolkit by project size?
Zustand for small to medium apps. Redux Toolkit for large teams, complex server state, and a need for RTK Query and time travel debugging.