npm i @blu3ph4ntom/inval
A dependency graph engine for incremental layout computation.
Not a framework. Not a renderer. The missing primitive.
The Problem
Sidebar resizes, dynamic-height rows, responsive breakpoints — applications have no abstraction for what geometry changed, and what depends on it.
Dashboard lag
Resize one panel. Every widget repaints. No dependency tracking means no optimisation.
Scroll jitter
Dynamic-height rows don't know their own dependencies. Total height recalculates on every scroll event.
Stale layouts
Manual memoization misses dependencies. A breakpoint change silently renders wrong data.
Invisible root cause
No tool surfaces which geometry invalidated and why. Debugging is trial and error.
Evidence — open issues in production libraries
The Solution
Inval models your layout as a directed acyclic graph. Inputs feed computed nodes. Change one value — only its descendants recompute.
import { input, node, why } from '@blu3ph4ntom/inval'
// Leaf inputs — external world writes here
const width = input(800)
const height = input(600)
// Computed node — lazy, cached, incremental
const area = node({
dependsOn: { w: width, h: height },
compute: ({ w, h }) => w * h,
})
area.get() // 480 000 — evaluated once
area.get() // 480 000 — cached, zero cost
width.set(1000)
area.get() // 600 000 — only area recomputed
why(area) // ['area', 'width'] Caching
Returns cached result if no dependency changed. No work done.
Incremental
Only the nodes downstream of what changed recompute — nothing else.
Debuggable
why(node) walks the graph and returns the exact invalidation path.
Zero deps
Pure TypeScript. 12 KB packed. No runtime dependencies, ever.
Features
Zero dependencies
Pure TypeScript. 12 KB packed. No runtime dependencies, no bundler surprises.
Framework agnostic
React, Vue, Svelte, vanilla JS. Ships as ESM, CJS, and an IIFE for script tags.
Lazy evaluation
Nodes compute only when accessed. Entire subgraphs can stay cold indefinitely.
Precise caching
A result is cached until exactly the inputs it depends on change. No over-invalidation.
Cycle detection
Circular dependencies are caught at construction time with a clear error message.
Debug tooling
why() traces invalidation paths. toDot() exports your graph for visualization.
Batch updates
Group multiple input changes into one atomic operation. One invalidation walk, not many.
TypeScript native
Full generics. InputNode<T>, ComputedNode<T>, all types exported and documented.
71 tests
Diamond dependencies, deep chains, cycle detection, batch semantics — all covered.
Use Cases
Virtualized lists
+Variable-height rows with O(1) scroll updates. Width change recalculates row heights. Scroll change recalculates only the visible range — row heights stay cached.
Resizable dashboards
+Drag a panel edge — only downstream widgets recompute their dimensions. Zoom changes hit only zoom-dependent nodes. Sidebar resize hits only width-dependent nodes.
Data grids
+Column resizing, row virtualization, dynamic cell heights — all tracked as a single DAG. Resize column 4, and only the cells and scroll positions downstream update.
Kanban & card boards
+Column widths, card heights, and scroll offsets form a graph. Add a card — only that column's height and offset recalculate. Other columns stay cached.
Whiteboards & node editors
+Drag a node — computed edge paths for only that node recalculate. Pan or zoom — bounding box and viewport transforms update incrementally.
Chat & feed UIs
+Message bubbles have dynamic heights. New messages only recompute from the insertion point down. Scroll lock and anchor positions stay accurate.
Performance
Versus a naive approach on 50 widgets with one width change: 417 K vs 4.23 M ops/s — 10× faster.
Start building
Drop inval into any project. Zero config, zero dependencies.
npm i @blu3ph4ntom/inval