Overview
Path-based dirty tracking for state trees, built on @dirtytalk/engine. Consumers wake only when a path they read changes.
@dirtytalk/structural tracks changes in objects and arrays. A dirty region is a set of interned path IDs, and each consumer subscribes to the paths it actually read. It is the path-set instantiation of the DirtyChannel from @dirtytalk/engine.
Why
Per-consumer diffing costs N consumers times one state walk per emit, repeating the same equality checks. Structural computes “what changed” once per emit, bounded to the observed skeleton (the union of paths any live consumer reads), then decides who cares with cheap set intersections. With few consumers the cost is similar; with many consumers sharing a container the saving grows with N.
Install
pnpm add @dirtytalk/structural @dirtytalk/engine
@dirtytalk/engine is a runtime dependency. The React adapter at @dirtytalk/structural/react needs react >= 18 (an optional peer dependency).
Minimal example
import { SyncScheduler } from '@dirtytalk/engine';
import { StructuralContainer, ALL_PATHS } from '@dirtytalk/structural';
interface CounterState {
count: number;
label: string;
}
class Counter extends StructuralContainer<CounterState> {
constructor() {
super({ count: 0, label: 'counter' }, { scheduler: new SyncScheduler() });
}
increment() {
this.patch({ count: this.state.count + 1 });
}
}
const counter = new Counter();
const unsubscribe = counter.subscribe(
() => ALL_PATHS,
(dirty) => console.log(counter.state, dirty),
);
counter.increment();
unsubscribe();
In React, a component reads state through useStructural; only the paths read during its render are recorded:
import { useStructural } from '@dirtytalk/structural/react';
const counter = new Counter();
function CountButton() {
const [state, container] = useStructural(counter);
return <button onClick={() => container.increment()}>{state.count}</button>;
}
CountButton reads count, so a change to label does not re-render it.
How it works
- A mutator (
emit,patch,update) replaces the state and marks the paths whose values changed on the container’s channel. - Consumers record the paths they read with a recording proxy (
trackRender) and register them as their interest. - On flush, only consumers whose interest intersects the dirty set are woken.
State is immutable: every change replaces objects along the changed path, and unchanged subtrees keep their reference.
Pages
- Containers:
StructuralContainer, mutators, subscriptions. - Tracking:
trackRender,ProxyCache, hazards, diff helpers. - React:
useStructural. - API reference: every export with signatures.
- Learn: structural: interactive walkthrough.
- Engine background: dirty channel, schedulers.