What Is Dependency Tracking?
Dependency Tracking is a technique used to automatically collect and record relationships between pieces of data. It allows the system to precisely trigger recomputation or side effects whenever the underlying data changes — making it the foundation of fine-grained reactivity.
A simple analogy is Excel:
When you change a cell, all other cells that depend on it recalculate automatically.
That is exactly the core idea of dependency tracking.
The Three Key Roles in Dependency Tracking
Reactive systems built on dependency tracking typically consist of three components:
1. Source (Signal)
A basic mutable value — the smallest unit of state.
2. Computed (Derived Value)
A pure function derived from sources, usually cached and lazily evaluated.
3. Effect (Side Effect)
Operations that interact with the outside world — DOM updates, data fetching, logging, etc.
These form a clear dependency graph:
How Dependency Tracking Works
Dependency Tracking is typically implemented in three steps:
1. Tracking (Collecting Dependencies)
When executing a computed or effect, the system uses a getter to record which sources were accessed.
These active computations are stored in a stack, which serves as the core mechanism for collecting dependencies.
A conceptual TypeScript example:
export interface Computation {
dependencies: Set<Set<Computation>>;
execute: () => void;
}
const effectStack: Computation[] = [];
export function subscribe(current: Computation, subscriptions: Set<Computation>): void {
subscriptions.add(current);
current.dependencies.add(subscriptions);
}
function createSignal<T>(value: T) {
const subscribers = new Set<Computation>();
const getter = () => {
const currEffect = effectStack[effectStack.length - 1];
if (currEffect) subscribe(currEffect, subscribers);
return value;
};
const setter = (newValue: T) => {
if (newValue === value) return;
value = newValue;
subscribers.forEach(sub => sub.execute());
};
return { getter, setter };
}
2. Notification (Re-running Dependencies)
When a source updates, the system notifies all dependent computations:
function effect(fn: () => void) {
const runner: Computation = {
execute: () => {
cleanupDependencies(runner);
runWithStack(runner, fn);
},
dependencies: new Set(),
};
runner.execute(); // Run once initially
}
function cleanupDependencies(computation: Computation) {
computation.dependencies.forEach((subscription) => {
subscription.delete(computation);
});
computation.dependencies.clear();
}
export function runWithStack<T>(computation: Computation, fn: () => T): T {
effectStack.push(computation);
try {
return fn();
} finally {
effectStack.pop();
}
}
Cleanup is crucial — without it, stale dependencies from old conditions would continue firing.
3. Scheduling (Batching & Optimization)
Schedulers prevent redundant execution and merge updates efficiently:
function schedule(job) {
queueMicrotask(job);
}
Different frameworks implement more advanced scheduling, but this is the fundamental idea.
Pull-based vs. Push-based Reactivity
A quick refresher, summarized:
| Type | Description | Examples |
|---|---|---|
| Pull-based | UI queries data changes (e.g., diffing) | Virtual DOM (React) |
| Push-based | Data pushes updates to dependents | Signals / MobX / Solid.js |
Signals-based systems typically adopt Push-based updates, which greatly reduce unnecessary re-renders and avoid global diffing.
Handling Dynamic Dependencies
A tricky aspect of dependency tracking is dynamic dependencies, such as:
effect(() => {
if (userId()) {
fetchProfile(userId());
}
});
If the condition changes, the system must:
- Stop tracking old dependencies
- Track new ones only when relevant This is why cleanup and stack-based execution are essential in most runtimes.
Framework Comparison: How They Implement Dependency Tracking
| Framework | Mechanism | Scheduler |
|---|---|---|
| Solid.js | Runtime, stack-based tracking | microtasks + batching |
| Vue 3 | Proxy-based runtime tracking | job queue (macro tasks) |
| MobX | Global wrapped getters | microtasks |
| Svelte | Compile-time static analysis | sync or microtask |
React’s dependency model is very different and will be examined in detail in the next article.
Conclusion
Dependency Tracking provides the fundamental architecture for fine-grained reactivity.
By automatically collecting relationships between data and computations, the system can update precisely and efficiently.
Mastering dependency tracking helps you design better UI architecture, optimize rendering workflows, and understand why modern reactive frameworks work the way they do.
Next Article: Dependency Tracking (II)
In the next chapter, we’ll analyze React’s dependency model, how it differs from fine-grained systems, and why Signals offer a cleaner solution.
Stay tuned!

Top comments (0)