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 (0)