Introduction
In the previous article, we broke down the core concepts and execution model of dependency tracking.
In this installment, we shift our focus to React’s dependency model—its characteristics, limitations, and how it compares to the explicit dependency graphs used by Signals.
What Is React’s Dependency Model?
React does not have a built-in explicit dependency graph. Instead, it operates using the following workflow:
-
State Updates (
setStateexecution)A state update triggers the entire component to re-execute its render function.
-
Re-rendering (Reconciliation)
React compares the new Virtual DOM tree with the previous one (diffing) to determine which parts need to be updated.
-
Effect Execution (
useEffect/useLayoutEffect)During the commit phase, effects are conditionally re-executed based on their dependency arrays.
This architecture represents a Pull-based + Scheduler-driven model:
React does not know which specific UI nodes depend on a given piece of state, so it must “pull” by re-running the entire component to ensure correctness.
Problems with React’s Dependency Tracking
| Issue Type | Description | Impact |
|---|---|---|
| No explicit dependency graph | React cannot precisely map state to UI segments | Unnecessary re-renders |
| Manual dependency arrays | Dependencies in useEffect must be manually maintained |
Easy to introduce bugs or redundant executions |
| Repeated computation | Multiple components reading the same state recompute independently | Performance waste, especially in large apps |
| Difficult dynamic dependencies | Conditional dependencies must be manually managed | Higher maintenance cost |
How Signals Improve the Situation
Signals (as seen in Solid.js, MobX, or Vue refs) address these issues in several ways:
-
Explicit dependency graph
Dependencies are automatically collected on
get, and only the relevanteffectorcomputednodes are notified onset. -
Fine-grained updates
Updates occur at the value level, not at the component level.
-
No manual dependency management
Dynamic dependencies are added or removed automatically at runtime, eliminating dependency array mistakes.
-
Reduced redundant computation
Shared computed values are evaluated once and reused wherever needed.
Side-by-Side Example
React
function Counter() {
const [count, setCount] = useState(0);
const double = count * 2; // Recomputed on every render
return (
<>
<h1>Hello World!</h1>
<button onClick={() => setCount(c => c + 1)}>
{double}
</button>
</>
);
}
What happens on count update:
-
Counterre-renders entirely -
Hello Worldre-renders unnecessarily -
doubleis recomputed every time
Signal
const [count, setCount] = createSignal(0);
const double = () => count() * 2;
function Counter() {
return (
<>
<h1>Hello World!</h1>
<button onClick={() => setCount(c => c + 1)}>
{double()}
</button>
</>
);
}
What happens on count update:
- Only
doubleis recomputed - Only the DOM node that consumes
double(the button) is updated
Why Does React Still Remain Dominant?
Despite lacking explicit dependency tracking, React mitigates these issues through several strategies:
- Virtual DOM diffing optimizations (e.g.
key,memo) - Automatic batching
- Selective rendering (
React.memo,useMemo,useCallback)
These mechanisms improve performance, but they also require manual optimization effort from developers.
Conclusion
From this discussion, we can draw the following conclusions:
- React follows a Pull-based model without an explicit dependency graph, which limits fine-grained updates.
- Signals adopt a Push-based model with an explicit dependency graph, automatically managing dependencies and updating only what is necessary.
- For performance-critical applications or systems with complex dependency relationships, the Signals model offers clear advantages.
In the next article, we will move into implementation details and gradually derive a simple Signal example from first principles.

Top comments (0)