Web Development

State Management Overload: When Redux, Context, and Others Collide

Choose state ownership before choosing a library, and stop the same fact from becoming several competing sources of truth.

3 min read

State management becomes difficult when the same fact has several owners. A selected customer lives in component state, a context provider, and a global store; a server response is copied into all three; then one update reaches only two of them. Adding another library does not resolve that disagreement.

The starting point is an ownership map. For each piece of data, identify who can change it, who must read it, how long it should survive, and whether it represents a local interaction or a fact owned by another system.

Separate different kinds of state

An open dropdown is local interaction state. A search query that should survive a copied link belongs in the URL. A draft form has an editing lifecycle. A fetched account balance belongs to the server and may be represented locally by a cache. These categories can cooperate without being stored in the same place.

For example, a product page can keep the expanded image locally, read the product identifier from the route, and use a request cache for inventory. A basket shared across routes may need broader ownership. Write these choices down before moving everything into a global store. The purpose is to make updates understandable, not to prove that one mechanism can technically hold every value.

Store the minimum facts and derive the rest

If a full name can be calculated from first and last names, storing all three creates an avoidable synchronization problem. React's state-structure guidance recommends avoiding redundant and duplicated state. The same principle applies to a selected object: often the stable identifier is sufficient, while the current object can be found from the authoritative collection.

Derived data still needs a clear calculation. If an order total depends on a price snapshot captured at checkout, it may be a historical business fact rather than a disposable sum of today's product prices. Decide whether a value is derived from current state or intentionally persisted as evidence. That distinction prevents well-meaning simplification from changing the product's meaning.

Use shared tools for specific coordination problems

Context can distribute values through a component tree. A reducer can organize related transitions. Redux can provide a shared state model with explicit actions and a predictable update path. The decision should follow the coordination problem and the team's ability to maintain it, rather than the popularity of the tool.

Selectors help keep derived calculations outside rendering components. Redux's documentation treats selectors as functions that read state and return a result, including filtered or transformed values. Keep those selectors close to the state model they understand. Components then ask for meaningful information rather than duplicating knowledge of the store's internal shape.

type Task = { id: string; completed: boolean };

function selectOpenTasks(tasks: Task[]) {
  return tasks.filter((task) => !task.completed);
}

// Keep tasks as the source of truth.
// Calculate the open list when it is needed.

Migrate one owner at a time

When simplifying an existing application, choose one troublesome fact and trace all its writers. Remove duplicate ownership gradually. Establish the new authoritative location, adapt its consumers, and only then delete the old copies. A temporary compatibility adapter is easier to remove than a permanent synchronization effect running in both directions.

Verify behavior that crosses boundaries: navigation, a failed save, a background refresh, and switching accounts. A state refactor is successful when these transitions become easier to explain. Fewer libraries can be a useful consequence, but the real improvement is knowing which piece of code has the right to change each fact and how everyone else observes that change.