Back to all articles
ReactZustandState ManagementJavaScript

The 5 Surprising Truths Zustand Taught Me About React State

Discover five key principles of modern React state management through the lens of Zustand, a small but powerful library that challenges conventions like Redux and the Context API.

The 5 Surprising Truths Zustand Taught Me About React State

The 5 Surprising Truths Zustand Taught Me About React State

If you're a React developer, you've felt the paradox of choice that comes with state management. The ecosystem is a crowded field of solutions, each with its own philosophy and trade-offs. We often start with native React tools, only to find ourselves navigating the performance pitfalls of Context or drowning in the boilerplate of libraries like Redux.

Then along comes Zustand. At first glance, it's a "small, fast and scalable bearbones state-management solution." It seems almost too simple. But as you dig in, you realize its elegant API isn't just a convenience; it's a masterclass in modern state management principles. It forces us to re-evaluate what we thought we knew, revealing fundamental truths that apply with or without this specific library.

As the library's own documentation puts it:

Don't disregard it because it's cute. It has quite the claws, lots of time was spent dealing with common pitfalls, like the dreaded zombie child problem, react concurrency, and context loss between mixed renderers.

Let's explore five of the most powerful lessons this little bear can teach us.

React's Built-in State Solutions Have Deceptive Downsides

1. React's "Simple" State Has Hidden Performance Traps

The first pattern we learn in React is "lifting state up." When two child components need to share a piece of state, we move it to their nearest common ancestor. It's intuitive, but it comes with a significant cost. Every time that shared state changes, the parent component must re-render. This triggers a cascade, forcing all of its children to re-render as well, even components that have no interest in the state that just changed. This can introduce serious performance bottlenecks in an application.

To avoid the "prop drilling" that comes with lifting state, our next logical step is often the React Context API. It feels like the perfect native solution—wrap a section of your app in a Provider, and any component inside can consume the state. The problem, however, is performance. The native Context API has a major drawback: any component consuming the context will re-render if any value within the context changes. It doesn't matter if your component only cares about state.user.name; if state.theme changes, your component re-renders anyway.

This is a surprising truth for many developers. The tools React gives us out of the box, while great for certain use cases, come with non-obvious performance traps. This is the exact problem Zust1.and was built to solve. Instead of forcing a component to re-render when any part of the state object changes, its hook-based selector model creates granular subscriptions. Your component will only re-render if the specific slice of state it selected has actually changed, eliminating the performance penalty of the native Context API.

2. You Can Have Global State Without Wrapping Your App in Providers

The performance traps of Context, as we just saw, are fundamentally tied to its Provider-based architecture. This leads to the next surprising truth: for truly global state, you don't need a Provider at all. With libraries like Redux or the native Context API, the first step is always the same: wrap your entire application, or a large part of it, in a component.

Zustand throws this convention out the window. You create your store as a simple hook, and that's it. You can then import and use that hook in any component, anywhere in your application, without any special setup in your component tree.

// store.js
import { create } from 'zustand'
const useBearStore = create((set) => ({
  bears: 0,
  increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
  removeAllBears: () => set({ bears: 0 }),
}))

// AnyComponent.js
function BearCounter() {
  const bears = useBearStore((state) => state.bears)
  return <h1>{bears} around here ...</h1>
}

This is impactful for several reasons. It dramatically simplifies your component tree by removing a layer of nesting. More importantly, it makes state truly global and decoupled from the React component lifecycle. You can access and update your store from utility functions, API layers, or any other part of your application that isn't a React component, something that is much more complex to achieve with provider-based solutions.

3. Adding Persistence Isn't a Chore; It's a Middleware

A common requirement in modern apps is persisting state across browser sessions. The go-to tool for this is localStorage. However, localStorage is not a replacement for a state management library; it's an extension that allows state to be saved and rehydrated.

The manual process for this is tedious and error-prone. You have to:

  1. Initialize your state by first trying to read from localStorage.
  2. Provide a default value if nothing is found.
  3. Create handler functions that, upon every state change, remember to call both your state setter and localStorage.setItem to keep them in sync.

Zustand reveals that this entire synchronization dance can be abstracted away. Adding persistence to a store is as simple as wrapping its definition in the persist middleware and providing a unique key for the storage item.

import { create } from 'zustand'
import { persist } from 'zustand/middleware'

const useFishStore = create(
  persist(
    (set, get) => ({
      fishes: 0,
      addAFish: () => set({ fishes: get().fishes + 1 }),
    }),
    {
      name: 'food-storage', // unique name for localStorage key
    }
  )
);

This single function call handles everything: initializing from storage, saving on every update, and ensuring your state and localStorage are always perfectly in sync. It's a prime example of how a well-designed API can make a complex task trivial.

4. The Best State is Less State

This takeaway is more of a philosophical principle, but it's one of the most critical lessons for writing clean, maintainable code. The official Zustand Tic-Tac-Toe tutorial provides a perfect illustration.

In the tutorial, during a "Final cleanup" phase, the xIsNext state variable is deliberately removed from the store. Why? Because it was redundant. Its value could always be calculated directly from another piece of state, currentMove. If currentMove % 2 === 0, then it's X's turn; otherwise, it's O's turn.

This is a profound insight. We often fall into the trap of storing every piece of data we might need, even if some of that data is derivable. The principle here is to store only the minimal, essential state required for your application to function. Everything else should be derived on the fly during the render. Zustand's minimal API encourages this line of thinking. Because you're not wrapped in boilerplate, you're more inclined to question what truly needs to be in the store versus what can be derived in the component. This approach leads to a single source of truth and eliminates an entire category of bugs where two pieces of state can fall out of sync. Your code becomes simpler, more predictable, and easier to reason about.

5. State Management Can Be a 'Choose Your Own Adventure'

Given its "bearbones" nature and small API, it's easy to assume Zustand is only for simple use cases. This couldn't be further from the truth. Its core is intentionally simple, but it's built to be extended through a rich ecosystem of middleware.

This makes Zustand a highly flexible, "un-opinionated" tool. It doesn't force a single pattern on you. Instead, it lets you opt into complexity as your needs grow.

  • Need to manage complex, nested state? Use the immer middleware to write simple, mutable-style update logic.
  • Miss the structure of Redux? Use the redux middleware to integrate traditional reducers and action types directly into your store.
  • Want to debug your state changes? Use the devtools middleware to connect your store to the familiar Redux DevTools extension.
  • Need to persist state to localStorage? As we've seen, the persist middleware handles this with a single line.

This "choose your own adventure" approach is one of Zustand's most surprising strengths. It provides a simple, frictionless entry point for new projects without locking you out of the powerful, advanced patterns you might need as your application scales.

Conclusion: More Than a Library, It's a Lesson

Zustand is an excellent state management library that can simplify your React applications. But its true value lies in the principles it embodies. It teaches us to be critical of unnecessary re-renders, to question the ceremony of providers, to abstract away repetitive tasks like persistence, to store less state, and to choose tools that scale with our needs.

While the tool itself is fantastic, the lessons it teaches about state management are what will truly make you a better developer.

Looking at your own projects, what piece of 'essential' state could be derived instead, and what complex logic could be simplified by the right abstraction?