DEV Community

Cover image for Dependency Tracking Fundamentals (I)
Luciano0322
Luciano0322

Posted on

Dependency Tracking Fundamentals (I)

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:

signal duty


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 };
}

Enter fullscreen mode Exit fullscreen mode

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();
    }
}

Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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());
  }
});
Enter fullscreen mode Exit fullscreen mode

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)