Audit BenchAi
← All posts

The State Management Chaos Vibe Coding Leaves Behind

·8 min read

Building a UI feature by feature, one prompt at a time, produces working screens quickly. It also produces, reliably, a frontend where the same piece of data — the current user, a cart total, a notification count — is held in more than one place at once, updated by more than one component, and occasionally shows two different values on the same screen. This isn't a framework problem or a skill problem. It's what happens when state decisions get made one component at a time, by a process with no visibility into the components that already exist.

Every prompt reaches for local state first

Asked to build a component, an AI assistant reaches for the smallest self-contained solution: a useStateinside the component that needs the value. That's the correct default in isolation — it's simple, and it doesn't require touching any other file. The problem surfaces only when a second component, built in a separate prompt, needs the same piece of data and gets its own local copy instead of a shared source, because the assistant building it had no reason to know the first component's state existed, let alone reuse it.

Prop drilling as the default wiring pattern

When a later prompt does need to connect two components, the fastest fix — and the one an assistant will reach for without being asked otherwise — is passing the value down through props from whatever ancestor already has it. One layer of this is normal React. Five or six layers of components passing a prop through that they don't themselves use is a sign that the state actually belongs somewhere more central — context, a store, a query cache — and that no single prompt in the feature's history had enough visibility into the component tree to notice that at the time.

Two sources of truth that quietly drift

The more damaging version of this isn't inefficient wiring — it's genuine duplication: server data fetched and cached in two different components instead of one shared query, a form's draft value held both in local component state and in a global store, or a "current user" object fetched independently by three different features. Each copy updates correctly in response to its own events and incorrectly stays stale in response to everyone else's. The bug that results — "the sidebar still shows the old name after I changed it in settings" — is a state-architecture problem wearing the costume of a UI bug, and it won't be fixed by patching the sidebar.

Why this is hard to catch feature by feature

A single component reviewed on its own always looks reasonable — it fetches what it needs and renders it. The defect only exists in relation to other components elsewhere in the tree that fetch or hold thesamelogical value independently. Catching it requires asking a question no individual diff review naturally raises: does this value already exist somewhere else in the app's state, and if so, why is this component getting its own copy instead of subscribing to it.

What to ask for in review

  1. Before approving a new piece of local state, check whether the same logical value — user, cart, auth status, feature flags — already exists elsewhere as shared state.
  2. Treat props passed through more than two or three intermediate components that don't use them as a signal to lift the state to context or a store, not as an acceptable wiring cost.
  3. For server data specifically, prefer a shared fetching/caching layer (a query library, a single store slice) over each component fetching independently, so "stale in one place" becomes structurally impossible rather than a bug to chase down later.
  4. When a bug report describes inconsistent values on the same screen, look for duplicated state before assuming a rendering or timing bug.

Read how to review generated code for production risk →

See why small diffs improve security review →