Two families of frontend frameworks disagree about who is responsible for updating the screen when data changes. Vue and Svelte answer the framework: the developer declares state, mutates it, and the dependency tracking figures out what to redraw. React answers the developer: state changes are explicit, the render function is called again, and the reconciliation decides what the DOM does. Both families ship products at enormous scale, so the disagreement is not about correctness, it is about where bugs go to live and what debugging feels like at two in the morning. This article maps the two mental models, walks the failure modes unique to each, compares the debugging workflows honestly, and ends with the team fit test, because the best framework is frequently the one the team's mental model already matches.

The dependency tracking model and what it promises

Vue wraps reactive state in a tracking layer that records which computed values and effects read which properties. A mutation of the state triggers exactly the dependents that read it, and nothing re-renders that did not:

import { ref, computed } from 'vue'

const query = ref('')
const items = ref([])

const filtered = computed(() =>
  items.value.filter(i => i.name.includes(query.value)))

The filtered list updates when query or items changes, because the computed tracked its reads at execution time. Svelte compiles the same idea away: assignments to declared state become fine grained signals, and the compiler emits the update code inline, with no runtime interceptor at all in its current generation, so the developer writes assignments and the generated code updates exactly the touched bindings. The promise of the family is locality: change a value, and the minimal set of screen fragments re-renders, with no manual memoization, no dependency arrays, and no render function re-executed for unrelated reasons.

The second promise is ergonomics for derived state. A computed or a derived value is an ordinary declaration, and chains of derivations read like a spreadsheet, totals from lines, discounts from totals, taxes from discounts, each a one line expression. The framework keeps the chain consistent under any change from any source, which removes an entire genre of stale state bugs before they can exist.

The explicit model and what it demands in exchange

React makes the render the unit of change. State changes through setters, every change calls the component function again with the new state, and the returned tree is reconciled against the current one:

const [query, setQuery] = useState('')
const [items, setItems] = useState([])

const filtered = useMemo(
  () => items.filter(i => i.name.includes(query)),
  [items, query])

The model's honesty is its strength: data flow is visible, updates happen because a setter ran, and no hidden dependency graph decides anything behind the source. The price is that the developer supplies what the framework in the other family derives. The useMemo call above names its dependencies by hand, and a missed dependency is a stale value bug that React cannot detect at runtime, it renders exactly what the array says. Effects, the synchronization of external systems with component state, carry the same arrays and the same sharp edge, plus the cleanup discipline for subscriptions and timers.

The model composes with immutability, state is replaced, not mutated, and time travel debugging, memoization by value identity and concurrent rendering features all stand on that replacement discipline. What the developer trades for the explicitness is exactly what the reactive family automates: the bookkeeping of what depends on what, written in dependency arrays instead of tracked by the runtime.

One invoice component written in both models

The differences concentrate when a component carries a derivation chain. An invoice editor with lines, a subtotal, a discount and a tax, in the reactive style, is a column of one line declarations:

const lines = ref([{ price: 100, qty: 2 }])
const discountPct = ref(0)

const subtotal = computed(() =>
  lines.value.reduce((s, l) => s + l.price * l.qty, 0))
const discount = computed(() => subtotal.value * discountPct.value / 100)
const total = computed(() => subtotal.value - discount.value)

Nothing else exists. The developer mutates a line, lines.value[0].qty = 3, and the subtotal, the discount and the total update through the tracked chain, and the template shows the new numbers because it read them. The same component in the explicit style:

const [lines, setLines] = useState([{ price: 100, qty: 2 }])
const [discountPct, setDiscountPct] = useState(0)

const subtotal = useMemo(
  () => lines.reduce((s, l) => s + l.price * l.qty, 0), [lines])
const discount = subtotal * discountPct / 100
const total = subtotal - discount

The chain itself reads almost identically, which is the honest point the comparison arrives at: for derived data, the two models converge to the same arithmetic. The divergence is in the mutation, setLines with a mapped copy that replaces the array, versus the direct assignment, and in what the reviewer must verify: in the first, that every derived value reads everything it should, in the second, that every memo array matches the reads and every setter replaces rather than mutates. The same defect, a discount that fails to update after editing a line, has two different root causes in the two files, a branch that skipped a read versus a dependency array missing lines, and two different debugging paths to the same fix.

Where bugs go to live in each model

The reactive family's characteristic bugs come from the automation being invisible. A computed that reads state conditionally tracks only the properties it read this time, so a branch that reads less state today changes which updates fire the value tomorrow, a subtle effect a reviewer can miss in a diff. Effects with implicit dependencies can form cycles, state A updates an effect that writes B, whose effect writes A, and the framework's loop detection breaks the cycle with a warning that appears in a console nobody was watching. Deep mutation through escaped references, a reactive object handed to a library that mutates it, updates things the team never declared as updates, and the fix is a readonly guard or a clone at the boundary.

The explicit family's bugs live at the boundary of the declared arrays. The missed dependency, the stale closure inside a callback built once with useCallback, the effect that runs twice because a dependency is an object identity that changes every render, are all the same mistake in different costumes: the developer's list and the code's actual dependencies disagree. The other genre is over-rendering, a parent state change re-running a child component tree for no visible reason, which costs performance rather than correctness and is fought with memoization, and the fight is why the memoization vocabulary is so large. Both families also share the ordinary bugs of state placed too high, props drilling and the resulting mystery of who owns what, which is a design problem no model solves by itself.

Debugging the two models at two in the morning

The debugging workflows differ in where the tooling points. In the reactive family the devtools show the dependency graph directly: which computed depends on which state, which components subscribed, and an inspector that lets the engineer mutate state and watch the update ripple, pause it and trace it. The question "why did this update" has a mechanical answer in the tools, the dependency chain, and the same tools answer "why did this not update", the missing dependency that a computed never read because of a branch. The difficulty is that the graph is runtime truth, and the source code is a static approximation of it, so the engineer debugs against information that exists only in the running session.

In the explicit family the source is the truth, and the tools work accordingly: a component render is a pure function of declared state, and the question "why did this render" reduces to which setter ran, traceable with breakpoints on the render function itself. The debugging hazard is the flip side of explicitness, the arrays are claims, and the code trusts them, so the workflow involves reading dependency lists and diffing them against the actual reads in the function body, a manual audit the other family automated. The third party tools that flag suspicious dependencies exist precisely because the framework declines to have an opinion.

The team fit test that decides more than benchmarks

The honest selection criterion after correctness is which mental model the team already holds, because a framework fought against the team's instincts produces defects at a steady rate no benchmark captures. Teams from the object oriented and desktop application world often find the reactive model native, it is the observer pattern with machinery, and teams with a functional background often find the explicit re-render natural, it is a pure function applied to state. Both instincts are learnable, and both frameworks are productive in either set of hands after the learning period, but the period is shorter and the defect rate lower on the native side.

The second criterion is the product's shape. Densely derived interfaces, dashboards, spreadsheets, configurators with chained calculations, reward the reactive family, because the derivation chains are the product and the automation is doing real work. Event driven interfaces with complex external synchronization, real time editors, heavy canvas and WebGL integration, maps, tend to reward the explicit family, because imperative boundaries with external systems want the imperative escape hatch to be a door, not a hatch, and React's model treats the external world as a first class citizen via effects.

The working rules either way

Whichever model the team ships, the defect genres above are preventable with written conventions, and the list is short:

  1. State lives as low in the component tree as the interface allows, raised only when sharing is real;
  2. Derived state is declared, never mirrored, one source of truth per value, copies are bugs;
  3. In the reactive family, effects that write state are reviewed for cycles and wrapped boundaries stay readonly;
  4. In the explicit family, dependency arrays are reviewed against the function body, and objects in arrays are normalized or memoized;
  5. External subscriptions always have teardown, timers, sockets and listeners are cleaned up on unmount;
  6. The devtools session that explains "why did this update" is a scheduled exercise for new joiners, not a rite of passage.

The frameworks disagree about who tracks dependencies, and the disagreement is worth taking seriously precisely because both answers work. The team that knows where its model hides bugs, and writes the three conventions that close those specific hiding places, gets the productivity of its chosen family without inheriting the bug genres, and that knowledge, more than any benchmark, is what the choice was actually about.

State libraries as the models' natural extensions

The built in mechanisms cover component state, and the products with cross cutting state converge on libraries that extend each model in its own direction. The reactive family's natural store is Pinia, which is a composable in every observable way, a defineStore function whose getters are computed and whose actions are ordinary functions mutating tracked state, so the devtools dependency graph, the debugging workflow from the previous section, extends from components straight into the store without a seam. The explicit family's stores form a spectrum: Zustand keeps the setter discipline, state is replaced through set calls and selectors subscribe components to slices, Redux keeps the reducer discipline, every change is a dispatched action through a pure reducer, and the spectrum's trade is boilerplate against enforceability, with Redux strongest where every change must be auditable and Zustand strongest where the team trusts itself to call setters sanely.

The mapping is worth writing down because the mismatch produces the worst of both worlds: a Redux store behind a reactive framework drags action creators through code that wants assignments, and a mutable store behind React defeats memoization everywhere downstream, since the object identities the reconciliation depends on never change when the contents do. The library choice inherits the framework's model, the store is the model with a namespace, and the team that understands its model picks its store in one afternoon instead of migrating between three of them in a year.