When you build a mapping interface in Angular Signals, the naive approach puts everything in one computed:
readonly sections = computed(() => {
const data = this.resource.value();
const draft = this.draftSignal();
const collapsed = this.collapsedSignal(); // 👈 mistake
return data.map(section => resolve(section, draft, collapsed));
});
This looks clean. One signal, one truth. But the dependency graph doesn't care about your intentions. Every time the user toggles a section open or closed, collapsed changes, the computed re-runs, and resolve(), which does badge counting, mapping resolution, and fallback calculation across every row in every section, runs again in full.
On a feed with 50 columns and 200 values per column, that's a lot of work for a toggle.
The fix is a two-level graph:
// Level 1 — depends on data + draft only
private readonly resolvedSections = computed(() => {
const data = this.resource.value();
const draft = this.draftSignal();
return data.map(section => resolve(section, draft));
// isExpanded = false here, placeholder
});
// Level 2 — depends on Level 1 + collapse state only
readonly sections = computed(() => {
const collapsed = this.collapsedSignal();
return this.resolvedSections().map(section => ({
...section,
isExpanded: section.available && !collapsed.has(section.name)
}));
});
Now a toggle only invalidates Level 2. Badge counts, mapping resolution, validation: none of that runs. isValidToSave reads resolvedSections() directly, so it's also unaffected by toggles.
The broader principle: Angular's computed graph is only as good as your dependency boundaries. If a computed reads ten signals but only five of them are relevant to its output, the other five are noise, and that noise has a runtime cost. Designing the graph means deciding, explicitly, which signals belong together.
Glitch-free reactivity isn't a property you get for free. It's something you architect.
Originally published on ysndmr.com.
Top comments (0)