createSignal: Publisher-Subscriber Node Tracking Graphs
- Last updated
- Prerequisites:
- SolidJS basics (createSignal, JSX)
- Understanding of reactive programming concepts
- solidjs
- signals
- createSignal
- reactivity
- fine-grained
- tracking
Read at your depth
The practical view
const [count, setCount] = createSignal(0) returns a getter (count) and a setter (setCount). Read count() to get the value; call setCount(1) or setCount(c => c + 1) to update. Any code that reads count() inside a reactive context (JSX, createEffect, createMemo, createComputed) automatically re-runs when the value changes. This is 'fine-grained reactivity': only the exact consumers of count re-execute, not the whole component. Unlike React, the component function itself does NOT re-run — the JSX expressions that read count() are the reactive nodes.
The same idea in other frameworks
const [count, setCount] = useState(0) re-renders the whole component; Solid's signal only re-runs the exact expressions reading it.Angular 16+ signals mirror Solid's model; Angular runs change detection per component, Solid per node.Vue's ref() is a similar signal; Vue tracks at component effect granularity with a Virtual DOM diff, Solid skips the diff entirely.Svelte 5 $state compiles to signals; Solid keeps them as explicit runtime primitives rather than compiled syntax.Legacy vs modern
Component re-render vs fine-grained signal updates
A component-wide re-render re-executes everything; Solid's signals re-run only the expressions that read the changed value, with no diffing.
const [count, setCount] = useState(0); // ❌ OLD: setCount re-renders the whole componentconst [count, setCount] = createSignal(0); // ✅ NEW: only expressions reading count() re-runInterview gotchas
Context
This is the defining architectural question of Solid — interviewers want the compiled fine-grained model vs the component-as-render-function model, not just 'Solid is faster'.
The mechanical answer
In Solid, JSX is compiled at build time into granular computation nodes: each expression that reads a signal becomes its own reactive computation wired into the DOM update path. The component function is a plain setup function that runs once to create those nodes and returns a template/mounting description; it is never re-executed. When a signal changes, only the computations that registered that signal as a dependency are invalidated and re-run, updating the exact DOM nodes they own. React, by contrast, treats the component function as the render unit: every state change re-runs the whole function and diffs. Solid's model trades the render-function abstraction for precision, which is why it has no Virtual DOM and no reconciliation of the component tree.
Trap
The naive answer is 'Solid is just faster React'. The models differ fundamentally: Solid components are setup functions, not render functions — writing component logic that assumes re-execution (e.g. re-deriving state from props inside the body) behaves differently. Another trap: thinking JSX variables are reactive — only signal reads inside compiled expressions are tracked; a variable computed once and read in JSX does not update unless it is a memo or reads a signal inside the expression. Candidates who explain the compile-time transformation show real understanding.
Context
This probes the dependency tracking mechanics — the dynamic edge rewiring that makes Solid's graph precise — a core differentiator interviewers dig into.
The mechanical answer
When a computation runs, it executes inside a tracking scope: the currently-active computation is pushed onto a global owner stack, and every signal read registers the current computation as a consumer (and vice versa). On each re-run, the computation's dependency set is rebuilt from scratch: Solid clears the previous dependencies and re-registers the ones actually read during this evaluation. Signals whose consumers no longer read them are dropped from their observer lists, so stale edges never cause spurious updates. When a signal's value changes, its version bumps and it notifies its current observer list; effects are scheduled to run (batched in a microtask) and memos invalidate lazily. This dynamic graph is what guarantees 're-runs only when actually read'.
Trap
The naive answer is 'Solid subscribes to signals at creation and never updates'. Dependencies are rewired on every evaluation — a conditional read (if (x()) use a() else use b()) correctly switches which signals trigger the effect. Another trap: assuming reads outside a tracking scope create subscriptions — reads in event handlers or plain functions outside effects/memos/JSX are untracked and create no dependency. Candidates who describe the owner-stack tracking and dependency rebuild demonstrate real depth.