In modern frontend architecture, the word “state” has gradually become overgeneralized.
API payloads, user input, computed frontend values, request loading status, and even the result of a side effect are often all placed under the broad category of “state.” At the UI level, this seems reasonable—anything that triggers a UI update looks like state. And once something is considered state, we tend to put it into a component, a store, a ref, a reactive object, or whatever state management tool is at our disposal.
But this is exactly where the problem begins. When the semantics of State, Derived State, and Effects are blurred together, the boundaries of the system’s data flow gradually start to collapse.
State is Not a Catch-All Term
From an architectural perspective, I prefer to think of State as the source of truth within a data flow.
It is the starting point from which subsequent derivations are produced. State should not be a value computed from something else, a temporary cache produced by a side effect, or a duplicated copy created merely for rendering convenience. It is the true entry point of data into the system. It can be changed by external events, user actions, or asynchronous results, driving the rest of the data flow forward.
For example:
// This is real State: the source node in a data flow
const count = signal(0)
But the following case is different:
// This is Derived State: it depends on count and shouldn't own an independent lifecycle
const double = computed(() => count.get() * 2)
double is also data, and it may need to be rendered. But semantically, it is Derived State. Its existence depends entirely on count. It lacks an independent data source, so treating it as independently owned State introduces synchronization cost and the risk of Data Drift. Once we promote Derived State into normal State, the cost of synchronization and the risk of Data Drift immediately follow.
The Core Problem of Derived State: Misplaced Ownership
Almost every complex application requires Derived State—subtotals, form validation statuses, filtered lists, and so on. The computation itself isn't the issue. The real architectural question is: Who owns this Derived State?
When Derived State is written into Component State, it becomes a "fake source of truth" that must be manually synchronized. Consider this common React pattern:
const [users, setUsers] = useState([])
const [activeUsers, setActiveUsers] = useState([])
// Synchronize data through lifecycle and Effect
useEffect(() => {
setActiveUsers(users.filter(user => user.active))
}, [users])
This code splits a simple derivation into three disjointed parts:
- The source
- The duplicated copy
- The synchronization logic
This is where system boundaries blur. We are forced to ask a series of architectural questions:
- Is
activeUsersa source or a derived result? - What state is the system in during that tiny window after
usershas changed but before the Effect has run? - If the Effect is skipped due to a flawed dependency array, will the data become permanently out of sync?
When data correctness is no longer guaranteed by structural relationships, but instead relies on execution timing, the maintenance cost of the system skyrockets.
The Danger of Effects: The Illusion of Omnipotence
Effects are arguably the most easily abused mechanism in frontend development simply because they are convenient.
An Effect can sync data, call an API, subscribe to events, write to localStorage, manipulate the DOM, or write Derived State back into State. Because Effects can do almost anything, complex data-flow problems often end up being pushed into them.
- Looks like Derived State:
useEffect(() => {
const next = expensiveCalculation(source)
setResult(next)
}, [source])
- Looks like Async Work:
useEffect(() => {
fetchData(id).then(data => {
setData(data)
})
}, [id])
- Closer to an actual external Side Effect:
useEffect(() => {
if (data) {
analytics.track("data_loaded")
}
}, [data])
In real-world projects, these three distinct semantics are routinely crammed into the exact same mechanism. As a result, the Effect gradually devolves into a universal escape hatch.
Hard to express a data relationship? Use an Effect. Need to keep two states in sync? Use an Effect. Don't know where a workflow belongs? Just throw it in an Effect. In the short term, it's convenient. In the long term, it destroys your data-flow boundaries.
When Sync Logic Becomes an Effect, You Rely on Execution Order
The relationship between State and Derived State should be structural. Once the source data is known, the derived result should be deterministic:
source state -> derived state
But when Derived State is synchronized via an Effect, correctness no longer relies solely on dependencies; it relies on whether the Effect executes at the right time:
source state -> render -> effect -> set derived state -> render again
This path introduces unnecessary uncertainty. Effects are usually attached to a lifecycle or rendering process after the fact. This means updates might be delayed until after the render, fail due to stale closures, or clash with interleaved Effects.
This is one of the root causes of many async bugs and inconsistent data states. The issue isn't careless developers; it's a system forcing distinct data semantics into a single execution model.
Restoring Semantic Roles
I prefer to separate these concepts into three distinct roles:
State: The source data of the system. It can be changed by external events or user actions.
Derived State: Data derived from State or other Derived State. It should not be manually synchronized.
Effect: An action that produces impact outside the data graph after data changes.
A healthier data flow should look more like this:
State
↓
Derived State
↓
Effect / Render / Async Boundary
Once the data graph is established, it becomes obvious that not everything needs to serve the Render cycle.
Components Shouldn't Bear the Weight of All Data Semantics
Frontend architecture becomes heavy because Components are forced to do too much. They describe UI, but they're also expected to manage Local State, compute Derived State, fire Async Requests, execute Effects, and manually juggle useMemo to prevent rendering bottlenecks.
When we use lifecycles to patch data flow, it's a glaring symptom that the system lacks distinct data boundaries.
This Is Also Why I Started Rethinking signal-kernel
Initially, I thought I was just building a fine-grained reactive system—providing primitives like Signal, Computed, and Effect for precise updates. But its true architectural value isn't just "more granular updates."
The deeper value is this:
It establishes a data-flow model based on a Reactive Graph, making semantic boundaries clear again.
In this model, business logic and data flow are no longer owned by the UI framework:
- Signal holds Source State.
- Computed holds Derived State, ensuring derivations are automatically tracked, not manually synced.
- Effect handles actual side effects (I/O, DOM manipulation).
Once this Reactive Graph is established, Render is just another consumer. The data flow is structured inside the graph first, and the UI simply reflects the current slice of that graph.
Let Data Semantics Return to Their Proper Place
As frontend apps grow, state increases naturally. More state isn't the enemy—semantic confusion is.
When Source State, Derived State, Effect Results, Async Statuses, and Render Caches are all treated as the exact same thing, boundaries vanish. You lose track of the true source. You rely on memoization to hide unclear ownership.
The true challenge of frontend architecture isn't just updating the screen efficiently; it's giving every piece of data a clear semantic owner. When these boundaries are restored, the system evolves from "patching data through lifecycles" to being "driven by data relationships."
This is the true value of a Reactive Graph. It makes the invisible boundaries of data visible again.
Top comments (0)