DEV Community

abhilashlr
abhilashlr

Posted on

Global state management in React apps

Choosing the Right Way to Manage Global State in React Apps

When building modern React applications, one of the most common questions teams face is:

How do we manage state that needs to be accessed by deeply nested components?

It's tempting to reach for React's Context API or start prop-drilling, but as apps grow, these approaches often lead to performance bottlenecks and complex code paths.

In one of our recent projects, we had this exact situation:

  • React Query was already in place for server state (API-driven data).

  • Zustand was used for certain client-side needs (local storage, undo/redo).

  • External libraries brought in their own Context providers.

Now we needed a way to add a global, app-wide local state — accessible from any level of the component tree.

First Principles: Different Types of State

Before deciding how to manage state, it's important to separate server state from client state:

Server state: Data that comes from APIs (users, posts, settings). Best handled by React Query, which gives us caching, background updates, and invalidation out of the box.

Client state: Data that lives only in the app (UI toggles, local preferences, temporary form state). This is where tools like Zustand shine.

Why Not Context for Everything?

React Context has its place — things like theming, localization, and authentication are great fits. But for frequently changing state, Context introduces two problems:

  • Performance – any update can re-render the entire tree of consumers.

  • Scalability – as state grows, multiple contexts quickly become hard to maintain.

Why Zustand Works Better Here

Zustand is lightweight, scalable, and avoids the pitfalls of Context. Each component can subscribe only to the part of the store it cares about, so re-renders are isolated.

Here's a simple example:

// store/globalStore.ts
import { create } from "zustand";

type GlobalState = {
  theme: "light" | "dark";
  setTheme: (t: "light" | "dark") => void;

  isSidebarOpen: boolean;
  toggleSidebar: () => void;
};

export const useGlobalStore = create<GlobalState>((set) => ({
  theme: "light",
  setTheme: (t) => set({ theme: t }),

  isSidebarOpen: false,
  toggleSidebar: () => set((s) => ({ isSidebarOpen: !s.isSidebarOpen })),
}));
Enter fullscreen mode Exit fullscreen mode

Usage anywhere in the app:

import { useGlobalStore } from "@/store/globalStore";

function SidebarToggle() {
  const isOpen = useGlobalStore((s) => s.isSidebarOpen);
  const toggle = useGlobalStore((s) => s.toggleSidebar);

  return (
    <button onClick={toggle}>
      {isOpen ? "Close Sidebar" : "Open Sidebar"}
    </button>
  );
}
Enter fullscreen mode Exit fullscreen mode

No prop drilling. No unnecessary re-renders. Just clean, predictable state.

Best Practices and Closing thoughts

As React apps scale, state management decisions compound. The best approach is not to introduce new tools every time a new need arises, but to leverage existing patterns wisely:

  • Use React Query for server state – don't reinvent async state management.

  • Use Zustand for client/global state – scalable, performant, and easy to adopt.

  • Keep Context minimal – only for true cross-cutting concerns like theme or i18n.

  • Split Zustand stores into slices – avoid a monolithic "god store" by modularizing concerns (UI slice, preferences slice, etc.).

This combination keeps the app consistent, performant, and easy for engineers to maintain.


✨ Curious to hear from other engineers — how do you manage global state in your React apps?


Top comments (5)

Collapse
 
yrsingh profile image
Yash Raj Singh

Thanks for sharing, this was really helpful!!!
Just wondering, can Zustand handle theming, localization, and auth? What makes React Context a better choice for these cases?

Collapse
 
abhilashlr profile image
abhilashlr

Great followup question, @yrsingh. So here's my thoughts:

  1. Theming/Localisation/Authentication are usually taken care by 3rd party libraries like oidc clients, styled components, etc. So with regards to such 3rd party libraries, they wouldn't or shouldn't be dependent on a library like Zustand to implement data layers between your apps and the libraries. For such cases, it is always advisable to continue using React Context. For such scenarios, the data you set on the context are usually not going to change and therefore no re-render logic comes into picture.

  2. But imagine there are data stores that your app wants to store data for use cases like undo redo, holding state temporarily and then save to the API when user hits a submit button. In this case, using React Context is definitely leading to re-renders. Another option I could suggest is libraries like react-query, but modifying the state in react-query is just going to bloat your codebase.

Collapse
 
louis7 profile image
Louis Liu

Yeah. I'm curious about the performance. So instead of re-render the tree from root. It only re-renders the component that use the global state?

Collapse
 
kansoldev profile image
Yahaya Oyinkansola • Edited

This looks interesting. I am really looking forward to using tools like zustand when building React apps, but I haven't yet built a project that requires having to manage a global state. Where do you think this is necessary from your experience?

Collapse
 
abhilashlr profile image
abhilashlr

A classic usecase could be something like these:

  1. Imagine you build an app that has template builder, where your UI is responsible for storing states in the frontend and the data is simple to complex, and should be saved or sent over the API only when user hits a "Submit" button at the end. This is very hard to do using useState variables or store them in Context which, as mentioned in the article, will lead to re-renders.

  2. Another usecase where your app has a memorised left navigation that can be collapsed/opened - which needs to be stored in the Localstorage. And what if you want the state of the left navigation component to be consumed in another Component to determine custom renders. You can do this with localstorate, but you will have to deal with async effects and make sure you replicate this logic of restoring what was last state back into the app on boot, and so on. In zustand, all you have to do is, define the zustand store and use persist util which handles hydration on app boot, and set the last value back to localstorage and sync effeciently. And you just have to invoke your store method and get the value like

const { leftNavState } = useLeftNavStore((state) => state.leftNavState);
Enter fullscreen mode Exit fullscreen mode

There are many such usecases in our product that made Zustand our go-to store for such usecases.