Home » How to Structure Zustand Stores in a Growing React Native App
Technology Top companies

How to Structure Zustand Stores in a Growing React Native App

Zustand feels almost perfect when a React Native application is small. A developer creates one store, adds a few actions, and accesses the state from any component without providers or reducers.

Then the application grows.

Authentication, onboarding, permissions, payments, notifications, drafts, feature flags, and offline queues all enter the same store. Screens subscribe to broad objects, actions begin changing unrelated domains, and persisted state survives longer than it should.

The problem is rarely Zustand itself. The problem is that its simplicity makes weak architectural decisions easy to postpone.

The official Zustand documentation recommends the slices pattern for dividing large stores into smaller composable units. That is a useful starting point, but a production React Native application needs stronger boundaries than simply placing each slice in a separate file.

A Single Global Store Is the Wrong Default

A large useAppStore containing every piece of application state creates hidden coupling.

Consider a store containing the current user, cart, notification preferences,, notification preferences, onboarding progress, payment draft, and network status. Any action can technically update any part of that state. Developers must understand the entire store before making a local change.

The better default is to organize stores by business responsibility:

stores/
  auth/
    auth.store.ts
    auth.selectors.ts
    auth.types.ts
  cart/
    cart.store.ts
    cart.selectors.ts
  onboarding/
    onboarding.store.ts
  ui/
    ui.store.ts

A store should represent a coherent state machine, not an arbitrary collection of variables. Authentication belongs together because token refresh, logout, session expiry, and profile hydration affect one another. Cart state belongs elsewhere because it follows different rules and lifecycle events.

The question should not be, “Can these values live together?” It should be, “Must these values change together?”

If the answer is no, they probably need separate stores.

Server State Does Not Belong in Zustand

Many growing applications turn Zustand into a homemade request cache. That is usually a mistake.

API responses, loading states, retries, invalidation, refetching, and pagination belong in a server-state library such as TanStack Query. Zustand should own client-controlled state such as:

  • Authentication session metadata
  • Multi-step form progress
  • Local drafts
  • Filters that survive navigation
  • Feature-specific UI state
  • Offline command queues
  • Cross-screen workflow state

Copying every API response into Zustand creates two sources of truth. A mutation updates the server, but the store may retain stale data. Developers then build manual synchronization logic that a dedicated query library already handles better.

Zustand should coordinate the application. It should not impersonate a database cache.

Keep Actions Beside the State They Control

Separating actions into distant utility files may appear tidy, but it weakens ownership.

A domain store should define its state, transitions, and reset behavior together:

type CartState = {
  items: CartItem[];
  coupon: string | null;
  addItem: (item: CartItem) => void;
  removeItem: (id: string) => void;
  clearCart: () => void;
};

export const useCartStore = create<CartState>((set) => ({
  items: [],
  coupon: null,

  addItem: (item) =>
    set((state) => ({
      items: [...state.items, item],
    })),

  removeItem: (id) =>
    set((state) => ({
      items: state.items.filter((item) => item.id !== id),
    })),

  clearCart: () => set({ items: [], coupon: null }),
}));

Components should express intent by calling addItem() or clearCart(). They should not contain duplicated state-transition logic or directly manipulate unrelated store fields.

This makes business behavior easier to test and prevents screens from becoming informal state managers.

Selectors Are an Architectural Requirement

The following subscription is convenient but careless:

const store = useCartStore();

It subscribes the component to the entire store. Any change can trigger a render, even when the component uses only one field.

The component should subscribe only to what it needs:

const itemCount = useCartStore((state) => state.items.length);
const addItem = useCartStore((state) => state.addItem);

Stable selectors improve performance and reveal dependencies. A component selecting six unrelated fields is communicating an architectural problem.

Selectors should also contain reusable derivations:

export const selectCartTotal = (state: CartState) =>
  state.items.reduce(
    (total, item) => total + item.price * item.quantity,
    0
  );

Derived values should rarely be stored. Persisting both items and total invites inconsistency because one can change without the other.

Persist Less State Than Feels Convenient

Persistence is useful for sessions, drafts, onboarding progress, and offline work. It is dangerous when applied to the entire store.

Temporary modal state, loading flags, error objects, navigation decisions, and stale API results should not survive an application restart.

Each persisted store needs:

  • An explicit allowlist
  • A version number
  • A migration function
  • A reset path
  • A decision about encryption
  • A clear ownership lifecycle

Authentication state should reset on logout. Tenant-specific state should reset when the active organization changes. Draft state should expire when the underlying record is submitted or deleted.

Persisted state is application data. It deserves the same lifecycle discipline as any other stored data.

Use Slices Only When Domains Truly Share a Store

The slices pattern is useful when several concerns require one middleware pipeline or atomic cross-domain updates. It should not become an excuse to rebuild the giant global store with more files.

A composed store might make sense for a tightly connected checkout workflow:

type CheckoutStore =
  & CustomerSlice
  & AddressSlice
  & PaymentSlice
  & ReviewSlice;

Authentication, notifications, and a media editor do not need to share that store simply because all three use Zustand.

Separate stores create stronger boundaries and clearer resets. Slices create internal organization. They solve different problems.

Five Companies to Consider for Scalable React Native Architecture

This is an opinionated shortlist based on publicly demonstrated React Native depth, architecture work, ecosystem involvement, and production engineering capabilities. It does not claim that every company standardizes on Zustand for every application.

1. GeekyAnts

GeekyAnts is the strongest fit on this list for teams that want React Native product engineering combined with practical state and performance work. Its public service material specifically covers state-management tuning, offline-first architecture, optimistic interfaces, background synchronization, conflict resolution, and reusable component systems.

That matters because Zustand architecture cannot be evaluated in isolation. Store boundaries must account for unreliable networks, application restarts, native lifecycle events, and data synchronization. GeekyAnts appears best suited to teams that need architecture and implementation rather than a high-level mobile strategy exercise.

2. Callstack

Callstack is an obvious choice for organizations facing difficult React Native performance or platform-level problems. Its specialization in React Native gives it an advantage over broad consultancies that treat the framework as one item in a large service catalog.

Callstack is particularly compelling when state architecture is contributing to rendering problems, startup delays, or New Architecture migration complexity. It would be an expensive choice for a simple CRUD application, but a sensible one for a large mobile platform with measurable performance constraints.

3. Infinite Red

Infinite Red has deep roots in the React Native ecosystem and maintains Ignite, an opinionated React Native starter kit.

Its value lies in conventions. Growing teams often need a repeatable architecture more than another flexible abstraction. Infinite Red is a strong candidate for organizations that want experienced mobile engineers to establish patterns, improve developer experience, and prevent every feature team from inventing its own approach to state.

4. Software Mansion

Software Mansion is closely associated with major parts of the React Native ecosystem, including libraries used for animations, gestures, navigation, and performance-sensitive interfaces.

It is the right company to consider when Zustand is only one part of a more demanding interaction problem. Complex editors, gesture-heavy applications, media products, and highly animated interfaces need coordinated thinking across JavaScript state, the UI thread, and native execution.

5. Thoughtworks

Thoughtworks has moved React Native into the “Adopt” category of its Technology Radar and cites successful production use across complex applications.

Thoughtworks is the best fit for enterprises where mobile state architecture must align with broader platform boundaries, governance, APIs, and organizational design. It is less specialized than the React Native boutiques on this list, but stronger when the mobile application is one part of a large modernization program.

The Best Store Is the One With a Clear Exit

Every Zustand store should answer five questions:

  • What domain does it own?
  • Which components may read it?
  • Which actions may change it?
  • What data survives an application restart?
  • What event resets it?

If those answers are unclear, the store is already too broad.

Zustand scales well when teams resist its most tempting feature: the ability to put anything anywhere. Small domain stores, narrow selectors, explicit persistence, colocated actions, and a clean separation from server state create an architecture that can survive growth.

The library does not enforce those boundaries. The engineering team must.