ReactZustandState ManagementTypeScript

Replacing Prop Drilling with Zustand: A Practical Guide

A
Aquib Yazdani
May 15, 2026·7 min read

How Zustand's sliced stores and reusable selectors fixed the highest-churn screens in an enterprise arbitration platform — and why it beats both Redux and Context for mid-size apps.

State management is one of those decisions that feels low-stakes early in a project and high-stakes six months in. At AAA's dispute resolution platform, we inherited a React codebase with deeply prop-drilled state — filter configs, pagination state, and async data passed through 5–6 component layers. Refactoring to Zustand was one of the most impactful engineering decisions I made there.

The Problem With Deep Prop Drilling

Prop drilling isn't just an aesthetics problem. When state lives several layers up from where it's used, every intermediate component becomes a pass-through. Add a new field to that state and you touch files that conceptually have nothing to do with the feature. Worse, every re-render of the parent causes re-renders all the way down the tree — even for components whose relevant slice of state hasn't changed.

Why Zustand Over Redux or Context?

  • Zero boilerplate — no actions, reducers, or action creators
  • Fine-grained subscriptions — components only re-render when their specific slice changes
  • TypeScript-first — store type is inferred from the create() call
  • Works outside React — useful for utility functions that need to read state
  • No Provider wrapper needed — global store is just a hook

Sliced Store Pattern

For larger stores, Zustand recommends a sliced pattern — each domain of state is defined in its own slice and combined into one store. This keeps files manageable without fragmenting state into multiple separate stores.

Building something interesting?

I'm open to new projects, collaborations, and conversations.

Let's Talk
typescript
// filterSlice.ts
export type FilterSlice = {
  filters: CaseFilters;
  setFilter: (key: keyof CaseFilters, value: string) => void;
  resetFilters: () => void;
};

const defaultFilters: CaseFilters = { status: "all", type: "all", dateRange: null };

export const createFilterSlice = (set: SetState): FilterSlice => ({
  filters: defaultFilters,
  setFilter: (key, value) =>
    set((s) => ({ filters: { ...s.filters, [key]: value } })),
  resetFilters: () => set({ filters: defaultFilters }),
});

// store.ts
export const useCaseStore = create<FilterSlice & PaginationSlice>()((...a) => ({
  ...createFilterSlice(...a),
  ...createPaginationSlice(...a),
}));

Selectors Prevent Unnecessary Re-renders

The most important Zustand pattern for performance is using selectors. Rather than subscribing to the entire store, a component subscribes to only the specific fields it uses. Zustand compares the selector's output by reference equality on each state update, skipping the re-render if nothing changed.

typescript
// BAD — subscribes to entire store, re-renders on any change
const { filters, pagination } = useCaseStore();

// GOOD — only re-renders when filters.status changes
const status = useCaseStore((s) => s.filters.status);

// GOOD — selector for derived value
const activeFilterCount = useCaseStore(
  (s) => Object.values(s.filters).filter(Boolean).length
);

Results

After the refactor, the highest-churn screens (case filter panel + results table + pagination) went from being the buggiest to the most stable. Redundant re-renders were eliminated. New filter fields could be added in one file without touching intermediary components. And junior engineers could understand the state shape in seconds — just open the store file.

"The best state management solution is the one that makes state changes obvious and co-located with the components that care about them."

← All Posts
ReactZustandState ManagementTypeScript