Setting Up State Management (Zustand) for a React Application

As an application grows, ad-hoc state management slows down development and multiplies errors. We set up Zustand in React projects turnkey: we design store architecture, implement it, and hand over clear working rules to the team. As a result, time spent on feature enhancements is reduced, and regressions stop reaching users.

Development and maintenance of all types of websites:

Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Frequently Asked Questions

When Zustand in React stops coping with the task

Zustand is good exactly up to the moment when state remains flat and predictable. As soon as nested entities, derived values, and asynchronous updates from multiple sources appear in the store, symptoms begin to surface that are hard to dismiss as a "bad component."

The first sign is extra re-renders. A selector that returns a new object or array on every call breaks reference equality, and subscribed components re-render on any store change, even one unrelated to them.

The profiler shows dozens of renders where one was expected, and the cause is not React but the shape of the selector.

The second is races during state updates. Two asynchronous actions write to the same key, the order of responses is not guaranteed, and a stale result settles into the UI.

Without explicit control over update order and cancellation of outdated requests, this reproduces inconsistently and is almost never caught in debugging.

The third is implicit dependencies between modules. One screen reads a field that another writes, the connection is declared nowhere, and removing a "harmless" action breaks someone else's scenario. State stops being local, and the boundaries of responsibility blur.

The fourth is the growing difficulty of debugging state. When logic is smeared across actions, middleware, and subscriptions, reconstructing the chain of "who changed the value and when" is no longer possible without additional instrumentation.

It is precisely at this stage that teams usually start asking whether it is time to change their approach to state management.

Let's break down how this gets addressed: where discipline in selectors and store structure is enough, and where a different tool is needed.

What well-structured state management delivers

State predictability instead of "magic" inside components

When state lives in dozens of local hooks and is smeared across the component tree, any change turns into archaeology: it's unclear who changed a flag and when, why the list re-rendered, and where a stale server response came from.

We move state into a single store with explicit operations — every change has a name, a place, and a clear set of subscribers. The interface starts behaving consistently across repeated scenarios instead of "sometimes glitching."

Fewer interface regressions

With centralized state, related screens read the same data rather than each having its own copy. A fix in one place doesn't break a neighboring module: the update logic is described once and reused.

In practice, this reduces the number of interface regressions — the scenarios that used to "fall apart" after changes stay working, and tests cover the operations on state themselves rather than each button individually.

Build time and module scaling

Splitting the store into independent parts allows modules to be built and connected separately: a new section is added with its own state area without touching existing ones. This cuts build time — only the affected module is rebuilt, not the entire application.

As the number of screens and teams grows, the structure remains readable: you can see which part of the state belongs to whom and where to look for the source of a problem.

What this gives the client

Changes stop being risky: new functionality is connected predictably, and the interface behavior doesn't depend on the order of clicks. The team localizes defects faster because the state and its changes are transparent. Next, we'll look at how we build such a structure and what remains with you after the work.

Approaches to organizing state: a comparison

State in a React application is almost never homogeneous: some data lives inside a single screen, some is needed by several branches of the tree, and some is needed by the entire application at once.

A mistake at the start is dragging everything into a single mechanism: either inflating local states into global ones, or putting into a shared store what lives for a couple of seconds. Below are four real options that people choose between in practice.

Option When it fits Limitations
Local state Data is needed by one component or a couple of neighbors: forms, expanded panels, drafts As the tree grows, "prop drilling" through intermediate components appears
React context Rarely changing values for an entire branch: theme, locale, access rights Frequent updates re-render all consumers at once
External store Data is needed in different parts of the application and changes often: cart, filters, sessions Requires discipline: without rules the store turns into a dumping ground
Combined scheme A large application where all three cases coexist A clear boundary is needed: what is stored and where, otherwise the logic sprawls

In practice, the working configuration is the combined one: local state covers isolated scenarios, context provides stable settings, and the external store holds shared data.

Zustand is convenient in this role because it does not require wrapping the application in providers and allows subscribing a component to a specific slice — re-rendering does not affect the rest.

Reviewing the options is needed in order to choose not the "trendy" one, but the one that fits the load profile. Next we show how this scheme breaks down in a specific application.

How We Integrate Zustand into a React Application

We start the integration not with code, but with an inventory: where state currently lives, what is duplicated between components, which updates cause unnecessary re-renders. Without this step, any library turns into just another layer on top of chaos.

  1. State audit. We walk through the component tree and write down which data is shared, which is local, and what is pulled from the server cache. We separately flag the places where state is passed through several levels or synchronized manually — these are exactly the ones that produce the most errors as the application grows.
  2. Designing the store schema. We split state into independent slices by domain, not by screens. We determine what stays in the component's local state, what goes into the shared store, and what goes into the request cache. At this stage we lock down naming rules and the structure of actions so that the store reads the same way for everyone.
  3. Setting up Zustand. We connect the store and configure selectors and subscriptions so that a component re-renders only on its own data. We add intermediate layers for debugging and for persisting part of the state between sessions where it is justified.
  4. Scenario testing. We run through real user paths: authorization, returning to a page, concurrent updates, action rollback. We look at what happens with rapid repeated clicks and during desynchronization with the server.
  5. Handing over documentation. We deliver the store schema, the rules for adding new slices, and a checklist of typical mistakes. The client's team then extends the application without us, without breaking the architecture.

Each stage ends with an artifact: a state map, a slice schema, a configured store, a scenario report, and a document for developers. This order eliminates the main cause of state problems — decisions made along the way while writing code.

Artifacts that remain in the project

After implementing zustand react, what remains in the project is not "magic in the developer's head" but a set of files and rules that the team lives by going forward. Below is exactly what we hand over to the client and why each artifact is needed.

  • Store configurations — typed slices by domain (user, cart, filters), separation of server and client state, middleware for logging and persisting to localStorage with key versioning.
  • Data schemas — a description of entities and their relationships: which fields live in the store, what comes from the backend, where the normalization boundary lies. The schema answers the question "why is this here and not in the component."
  • Access policies — rules for who can change state and when: read-only selectors, actions with role checks, a ban on direct mutations outside the store. Separately, we record which data does not go into the store at all.
  • Documentation — a store map with the purpose of each slice, examples of wiring it into components, naming conventions for actions and selectors. A new developer is brought up to speed in hours, not weeks.
  • Regulations — when to create a new slice, how to review state changes, how to roll back an unsuccessful store release. This is what keeps the architecture from sprawling six months down the line.
  • State tests — coverage of key scenarios: hydration, reset on user logout, races during parallel requests. We test the store's behavior, not the internals of components.
  • Migration checklist — a step-by-step plan for moving the remaining parts of the application to the new store, with rollback points at every step.

Every artifact exists in the repository alongside the code — not in a chat thread and not in someone's head. Describe your task, and we will come back with an estimate and a plan: what will be included in the handover in your specific case.

Case: reducing build time and the number of regressions

The project is a customer portal with dozens of screens on React: global state grew chaotically, logic was smeared across contexts and local component stores. Every change to the data model dragged edits along in different places, and tests failed in waves.

We moved state management to Zustand and broke it down by domains — instead of one ever-growing store, independent slices with clear boundaries appeared.

What it was before

The build took about 14 minutes: the entire module graph was rebuilt because one shared context pulled dependencies into every screen. State regressions — on average 6-8 per release: races during updates, desync between tabs, lost values after navigation.

Recovery after an incident stretched into hours — there was no clear place to look at the current state and its history.

What we changed

  • Split state into domain slices: authentication, profile data, interface settings — each updates in isolation.
  • Moved side effects out of components into subscriptions to changes, removed duplicate requests.
  • Added selectors with memoization: a component re-renders only on its own data.
  • Set up persistence of part of the state between sessions and a reset on logout.

What it became

The build shrank to 5-6 minutes — incremental changes stopped touching the entire graph. The number of state regressions dropped to 1-2 per release, and most are caught at the testing stage, not in prod.

Recovery after a failure takes minutes: the state is predictable, it is visible, and it can be rolled back in a targeted way. Support costs stopped growing — new screens connect to ready-made slices instead of pulling in new logic.

How is Zustand different from Redux and MobX?

Zustand is a minimalist store for React: state lives in a plain object, a component subscribes to the slice it needs via a selector and re-renders only when that slice changes.

No provider or wrapper around the tree is needed; the store is accessible both in components and outside React — in handlers, utilities, tests. The learning curve is measured in dozens of lines: create a store, describe actions as plain functions, hook it up in a component.

Redux requires more ceremony: actions, reducers, dispatching, and in the typed variant — also working with middleware for asynchronicity.

This is justified where a strict change log, reproducibility, and step-by-step debugging are needed — for example, in applications with dozens of scenarios and several teams. But there is noticeably more boilerplate, and every new feature accumulates files.

MobX takes a different path: state is observable class fields, and components automatically react to their changes.

There is less code than in Redux, but "magic" appears — it is not always obvious why a component re-rendered, and debugging requires understanding the internal mechanics of reactivity.

In terms of performance, the difference shows up at scale. Zustand with pinpoint selectors re-renders only subscribed components, and on a medium-sized application this gives smooth responsiveness without manual optimization.

Redux with sloppy selectors starts re-rendering extra, and you have to add memoization. MobX handles frequent updates well, but the price is that it is harder to predict exactly what will update.

Debugging is a key selection criterion. Redux has ready-made tools with a timeline and rollback, which is invaluable when analyzing complex scenarios.

With Zustand, debugging is simpler by structure: the store is an object, changes are visible in an ordinary logger, but there is no ready-made time machine "out of the box." MobX wins in speed of writing, loses in transparency: the more reactive connections, the more expensive it is to analyze unexpected behavior.

In practice, the choice is this: Zustand — for most applications where development speed and predictable re-renders matter; Redux — for large systems with strict requirements for the change log and teamwork; MobX — for interfaces with intensive updates and a small development core.

We select the store for the task, not for the trend — and lay out the structure so that switching between approaches remains feasible.

Common technical concerns

Zustand is not a framework, but a small library on top of React: it does not dictate project structure and does not require wrapping the application in providers.

That is why it can be just as easily plugged into a brand-new project as into a codebase that has been alive for years. Below are the questions most often asked before getting started.

Is it compatible with our current React application?

Yes. Zustand works with any current React branch, including strict mode and server-side rendering, and does not conflict with existing providers.

A store can be connected selectively — to a single screen or module — and the scope of responsibility can be gradually expanded without rewriting the entire application.

How does migration from Redux or context work?

Migration proceeds in parts: first one independent piece of state is moved, then the next.

The logic remains readable, and the old and new approaches work in parallel for a while — this allows development to continue without stopping and makes it possible to roll back at any step without data loss.

Will performance degrade with large volumes of data?

On the contrary: a component re-renders only when the piece of state it needs changes, not the entire store. In practice, this eliminates unnecessary re-renders of lists and tables that cause the interface to start "lagging" during active user interaction.

What about support and ongoing development of the solution?

We leave the store structure, naming rules, and data flow scheme in the project documentation.

Any engineer on the team can add a new store or modify an existing one without having to figure out someone else's logic from scratch — support does not turn into a dependency on a single person.

If your case is not on the list — describe the task, and we will come back with an estimate and a work plan.

Let's discuss your state management challenge

To come back with an estimate and a work plan rather than generic words, we need an initial analysis of your application.

Describe the task in free form: where state is currently stored, which screens depend on it, what hurts — extra re-renders, data desync between tabs, unreadable update chains. That's enough to understand the class of solution.

Next, we'll ask targeted questions: how work with server data and cache is structured, whether there are long-lived forms, how many developers edit the same modules simultaneously, which scenarios are already covered by tests.

If part of the context is closed off — no problem, we'll gather what's missing on a call with your tech lead.

Based on the initial analysis, you'll receive:

  • a map of the current state: what data lives where and who owns it;
  • a list of bottlenecks indicating what each fix delivers;
  • a proposal for the store structure — where the shared store is, where the local one is, where the server cache is;
  • a phased work plan with the order of implementation and rollback points;
  • an estimate of effort and team composition for your scope;
  • migration risks and a way to transition the application without halting releases;
  • acceptance criteria: by which metrics you'll confirm things have improved.

We'll agree on the plan before the start — you see what changes and in what order, and you can adjust priorities. Describe the task in whatever way is convenient: text, a link to the repository, or a short call. We'll come back with an estimate and a work plan.

Our competencies:

Latest works

  • B2B ADVANCE company website development
    B2B ADVANCE company website development
    1542
  • Development of a web application for FEEDME
    Development of a web application for FEEDME
    1394
  • Website development for BELFINGROUP
    Website development for BELFINGROUP
    1081
  • Development of an online store for the company FURNORO
    Development of an online store for the company FURNORO
    1368
  • Development of a web application for Enviok
    Development of a web application for Enviok
    1139
  • Website development for FIXPER company
    Website development for FIXPER company
    1060