DEV Community

KhaledSalem
KhaledSalem

Posted on

When Does Local State Become Shared State? A React State Ownership Guide

State Management Decision Authority — Part 02

Your React state doesn't need to be global just because two components need it.

In Part 1, useState vs useReducer: Stop Choosing Tools Before Understanding State, I explored an architectural idea I call the State Management Decision Authority: choosing state management strategies through consistent criteria rather than tool preferences.

One of those criteria deserves a deeper discussion:

State Ownership.

Because sharing state and globalizing state are two very different decisions.

1. The Checkout Problem

Imagine a checkout feature with three components:

function Checkout() {
  return (
    <>
      <ShippingForm />
      <OrderSummary />
      <PaymentSection />
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Each component has a different responsibility.

  • ShippingForm collects the delivery address.
  • OrderSummary displays the shipping cost.
  • PaymentSection checks whether checkout requirements are satisfied.

But they all depend on some of the same information.

If the user changes the shipping country, the available shipping methods might change. The order summary may need to recalculate its totals, and payment might need to be disabled until a valid shipping option is selected.

The three components now need to coordinate.

Does that mean we should introduce a global store?

Not yet.

2. First Principle: Ownership Before Accessibility

Before choosing a tool, ask:

Who should own this state?

This is different from asking who needs to read it.

A component may consume state without owning its lifecycle or controlling its transitions.

For our checkout example, the checkout feature itself is a reasonable ownership boundary.

We can lift state to the nearest common ancestor:

import { useState } from "react";

function Checkout() {
  const [country, setCountry] = useState("EG");

  return (
    <>
      <ShippingForm
        country={country}
        onCountryChange={setCountry}
      />

      <OrderSummary country={country} />

      <PaymentSection country={country} />
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Now we have a single source of truth for country.

No Zustand. No Redux. No global Context provider.

Just state ownership aligned with the component tree.

Of course, the example is intentionally simplified. A real checkout flow would also need shipping options, validation rules, and order totals. Those can be derived or managed within the same feature boundary as appropriate.

Sharing state doesn't automatically require global ownership.

3. The Smallest Sufficient Ownership Boundary

Here's a principle I find useful:

Give each piece of state the smallest ownership boundary that satisfies its requirements.

Why the smallest?

Because expanding the ownership boundary also expands the potential dependency surface.

More components may gain access to the state. More logic may begin relying on its shape and behavior. Changes to that state can become harder to reason about.

That doesn't make shared state inherently bad.

It means accessibility should be intentional.

For example:

Requirement Potential ownership
Toggle a local dropdown Dropdown component
Coordinate shipping and payment Checkout feature
Share a wizard's progress across steps Wizard boundary
Display a notification across independent features Shared notification service or store
Cache fetched product data Server-state cache

Notice that ownership isn't determined by the number of components alone.

It's determined by the scope of coordination.

4. When useReducer Still Belongs Inside a Feature

What if our checkout state becomes more complicated?

Changing the shipping country might invalidate the selected shipping method.

Applying a discount might update the totals.

Submitting the checkout might require a valid combination of address, shipping option, and payment information.

Now we have related transitions and invariants.

This might justify useReducer:

const initialState = {
  country: "EG",
  shippingMethod: null
};

function checkoutReducer(state, action) {
  switch (action.type) {
    case "COUNTRY_CHANGED":
      return {
        ...state,
        country: action.country,
        shippingMethod: null
      };

    case "SHIPPING_SELECTED":
      return {
        ...state,
        shippingMethod: action.method
      };

    default:
      return state;
  }
}
Enter fullscreen mode Exit fullscreen mode

In a production implementation, SHIPPING_SELECTED should also validate that the selected shipping method is permitted for the current country, or accept only previously validated options.

The important architectural detail is that introducing useReducer changes how we manage transitions, not who needs to own the state.

We can continue keeping the reducer inside Checkout.

A complex state is not necessarily a global state.

5. When Context Becomes Useful

Now imagine our checkout feature grows deeper:

Checkout
  └── CheckoutLayout
       └── CheckoutSteps
            └── ShippingStep
                 └── ShippingForm
Enter fullscreen mode Exit fullscreen mode

Passing checkout state through components that don't use it can become inconvenient.

This is where Context might help.

import {
  createContext,
  useContext,
  useReducer
} from "react";

const CheckoutContext = createContext(null);

function CheckoutProvider({ children }) {
  const [state, dispatch] = useReducer(
    checkoutReducer,
    initialState
  );

  return (
    <CheckoutContext.Provider value={{ state, dispatch }}>
      {children}
    </CheckoutContext.Provider>
  );
}

function useCheckout() {
  const context = useContext(CheckoutContext);

  if (context === null) {
    throw new Error("Missing CheckoutProvider");
  }

  return context;
}
Enter fullscreen mode Exit fullscreen mode

We can place CheckoutProvider around the checkout feature rather than the entire application.

Context solves an accessibility problem: delivering a value to consumers deeper in a component tree.

It doesn't automatically solve complex transitions, and it isn't inherently a rendering optimization.

When a provider value changes, components consuming that Context are eligible to re-render. In performance-sensitive flows, we may need to split contexts, stabilize values, or use more selective subscription mechanisms.

Context changes how state is accessed. It doesn't require changing who owns it.

6. When an External Store Starts Making Sense

Suppose the requirements evolve again.

Several independent application features now need to coordinate shared client state.

Maybe a shopping cart is modified from product listings, product detail pages, and a persistent cart drawer.

A dedicated shared store may become easier to maintain than repeatedly lifting state or creating an increasingly broad Context.

This is where Zustand or Redux Toolkit becomes worth evaluating.

But the decision shouldn't be:

"We have five components consuming this state, so let's use Zustand."

A more useful question is:

Do we now have an ownership and subscription problem that our existing structure handles poorly?

Other considerations include:

  • Whether consumers need selective subscriptions
  • Whether cross-feature updates are becoming difficult to coordinate
  • Whether state transitions need stronger conventions or debugging capabilities
  • Whether the project already has a suitable established state management architecture
  • Whether the additional abstraction reduces more complexity than it introduces

An external store may be the right solution.

But it should be the result of a concrete architectural requirement.

7. Shared Client State Isn't Server State

There's one more boundary worth preserving.

Some data is shared because it originates from the server.

Product catalogs, user records, and remote inventory are examples.

That introduces concerns such as caching, stale data, refetching, invalidation, and synchronization.

For those cases, a server-state solution such as TanStack Query or RTK Query may be more appropriate than manually copying remote data into a general-purpose client store.

This isn't an either-or decision.

A real application can use local React state for a form, a shared client store for a cart, and a server-state cache for product information.

The important thing is to avoid giving one mechanism responsibilities it wasn't chosen to handle.

We'll explore this distinction later in the series.

8. A State Ownership Decision Framework

Before changing your state architecture, try answering these questions:

Question Possible direction
Does only one component need the state? Keep it local
Do siblings need to coordinate? Lift to their nearest common owner
Is passing data through intermediate components becoming cumbersome? Consider composition or Context
Are related transitions becoming difficult to maintain? Consider useReducer within the existing boundary
Are independent features coordinating shared client state? Evaluate an external store
Is the state primarily remote data? Evaluate server-state management

The framework is deliberately not a rigid decision tree.

These dimensions overlap, and different solutions can satisfy the same requirements.

Its purpose is to make our reasoning explicit.

Final Thought

One of the most expensive architectural decisions is making a local concern global before it needs to be.

Once unrelated features begin depending on a shared state boundary, changing that boundary can become harder.

My principle is simple:

Sharing state is a requirement. Globalizing state is an architectural decision.

And the goal should be:

The smallest sufficient ownership boundary.

That's how I'm applying the State Management Decision Authority introduced in Part 1.

Not by standardizing every tool, but by standardizing how we evaluate the problem.

Unify the decision criteria, not necessarily the tools.

What's next?

Part 03: Context vs Zustand vs Redux Toolkit — What Actually Justifies a Shared State Solution?

We'll go beyond feature ownership and explore how subscription behavior, transition complexity, tooling, and application boundaries influence the choice.

What makes you decide that React's built-in state management is no longer enough for a particular feature?

Top comments (1)

Collapse
 
davekurian profile image
Dave Kurian •

The COUNTRY_CHANGED reset is a useful invariant. I’d also test restore: a saved shipping method may fit the old country but fail for the current one, so validate it before payment becomes ready. Do you plan to cover that in the transition tests?