The Solid Mental Model
The component function runs once. Signals hold values with subscriber lists, and setting one re-runs only the small computations that read it — each writing to the node it owns.
The intent, the obvious build, and why it breaks
Every lesson starts where the work starts: someone wanted an outcome, and the first implementation that comes to mind has a problem.
What changes when the component function never runs a second time?
Someone watches a live dashboard where one number updates several times a second. They expect that number to move and everything else on the page to stay perfectly still.
Signals are a nicer state API. You call a getter instead of reading a variable, but otherwise the component works like any other component: it re-runs when state changes.
It does not re-run. The component function is setup code: it executes once, builds nodes, wires computations around the dynamic parts and returns. Anything written expecting a second pass never gets one.
- It does not re-run. The component function is setup code: it executes once, builds nodes, wires computations around the dynamic parts and returns. Anything written expecting a second pass never gets one.
- That inverts the usual advice. There is nothing to memoize at the component level, because the component body is not on the update path — a memo there is overhead in exchange for nothing.
- Reading a signal is a function call for a reason: the call is what registers the subscription. Destructuring props, or pulling a value into a plain variable in setup, unsubscribes silently — the same class of bug as losing a proxy read, with the same absence of any error.
- Conditionals need to be written so the framework can re-evaluate them. An ordinary
ifin the component body runs once, at setup, and picks a branch permanently. - Cleanup is owned by the reactive scope rather than by a lifecycle method, so "when does this get torn down" is a question about which computation owns the subscription.
What is actually happening
In the browser, not in the framework.
- A signal is a value plus a subscriber list. Reading it through its accessor, inside a tracking scope, adds the current computation to that list. Setting it notifies them.
- Computations are the unit of re-execution. A derived value, an effect, or the small binding created for a dynamic expression in markup — each is a computation with its own dependency set, and each re-runs alone.
- Markup compiles to node creation, not to a description. A dynamic expression becomes a real DOM node plus a computation that writes to it. There is no tree to build per update and none to compare (The DOM Is Not Your HTML).
- The component function is setup. It runs once to create nodes and computations. Its local variables are created once, which is why a plain read at that moment captures a value rather than a dependency.
- Control flow is reactive by construction. Conditional and list rendering are handled by framework components whose job is to subscribe to a condition or a collection and rebuild the appropriate part when it changes.
- Propagation is ordered. Derived values are updated before the effects that read them, so an effect does not observe a half-updated graph (Derived State).
What this makes the browser do
And which of it is avoidable.
- One pass of node creation per component instance, then nothing at the component level for the rest of its life.
- Per update: run the subscribed computations and write to their nodes. The work is proportional to the number of bindings that read the changed value, not to the size of the tree.
- Memory holds the graph: one subscription record per binding, retained for the lifetime of the view. Cheap per update, not free at rest (Memory Leaks).
- DOM calls and their downstream style, layout, paint and composite costs are unchanged — this model reduces JavaScript, not browser work (The Cost of a Change).
- Avoidable work: a derived value recomputed by many consumers instead of being memoized once, or a coarse signal holding an object where several narrow signals were wanted.
Runs once, updates forever
The code below is short and every line of it depends on the same fact: the function body executes exactly once. What survives afterwards is the graph it built — signals, derived values, effects, and the little computations attached to each dynamic expression in the markup.
That is why the two commented lines are bugs rather than style. Each performs its read during setup, and a read during setup produces a value. The reactive versions defer the read to a place that will run again.
1import { createSignal, createMemo, createEffect } from 'solid-js'2 3function Counter(props: { step: number }) {4 const [count, setCount] = createSignal(0)5 6 // Re-evaluated when `count` changes: the read is inside the memo.7 const doubled = createMemo(() => count() * 2)8 9 // BUG: reads once, during setup. Never updates.10 // const frozen = count() * 211 12 // BUG: destructuring props performs the reads now,13 // so `step` stops tracking its source.14 // const { step } = props15 16 createEffect(() => {17 // Subscribes to `count` because it read it while tracking.18 document.title = `Count: ${count()}`19 })20 21 // The JSX compiles to node creation plus one computation per22 // dynamic expression. This function will not run again.23 return <button onClick={() => setCount(count() + props.step)}>{doubled()}</button>24}The distinction throughout is between reading a value and creating a subscription. Both look like a function call; only one of them is inside something that will run again.
The graph is the program
In a re-render model the runtime shape is a tree of components. Here it is a directed graph of computations: signals at the sources, derived values in the middle, and effects and DOM bindings at the leaves. An update is a traversal of the part of that graph reachable from one node.
Reading the graph explains the model's two headline properties at once. Update cost is proportional to reachable subscribers rather than to tree size — that is the speed. And a node that never got an edge is unreachable forever, with no error to report — that is the failure mode.
The habits that break it
Every row here is the same underlying mistake — performing a read where it will not be performed again — dressed in a different piece of ordinary JavaScript. That is what makes them hard: each one is idiomatic code that happens to be wrong in this model.
| Trigger | Symptom | Cause | Response |
|---|---|---|---|
| Destructuring props | A prop is correct on first paint and never changes | Destructuring evaluated the accessors immediately | Access through the props object at the point of use. |
A plain if in the component body | One branch renders forever regardless of state | The body ran once, so the branch was chosen once | Use the framework's conditional component, which subscribes to the condition. |
| Mapping an array directly in markup | The list never updates, or rebuilds entirely on any change | A plain map runs once, or produces a new array with no identity information | Use the list component, which subscribes to the collection and reconciles by key (Reconciliation and Keys). |
| Reading a signal in a timer or a promise callback | The value is stale, or the effect does not re-run | The read happened outside any tracking scope | Read inside a computation, or pass the value in explicitly. |
| One signal holding a large object | Everything updates when any field changes | Granularity is defined by signal boundaries | Split into narrower signals, or use a store designed for nested tracking (The Seven Kinds of State). |
How to build it
Most important first.
- Read signals where they will be read again: inside markup, inside a derived value, inside an effect. A read in setup is a value, not a dependency.
- Do not destructure props. Access them through the object so the read happens at use time; this is the single most common source of lost reactivity here.
- Use the framework's control-flow components rather than plain JavaScript conditionals and array methods in markup, because those components are what subscribe to the condition or the collection.
- Memoize derivations that are expensive or shared, not components. The unit of optimisation in this model is the computation (Memoization).
- Keep signals narrow. One signal per independently changing value gives the model the granularity it exists to exploit (The Seven Kinds of State).
Keyboard, focus, semantics, announcement
A required field on every lesson in this domain, not a section added when there is room.
- This is the gentlest model for assistive technology by construction: text is written into existing nodes, so node identity, focus, text selection and scroll position all survive updates (Node Identity Across Updates).
- Control-flow components still remove nodes when a condition flips, and a removed node that held focus sends focus to the body silently (Focus Management).
- Because updates are so surgical, they are also very quiet. A value that changes with no announcement is invisible to a screen-reader user; live regions are still your responsibility (Live Regions and Announcement).
- High-frequency updates that are cheap enough to do sixty times a second will flood a live region if one is attached. Throttle what is announced separately from what is displayed (Live Regions and Announcement).
- Keyed list rendering has the same identity requirements as everywhere else, and the same consequence when it is wrong (Reconciliation and Keys).
What can go wrong
- Lost reactivity from destructuring, from an early return, or from reading a signal outside any tracking scope. The UI shows the value at setup time, forever, with no error.
- A component body written as if it re-runs — deriving values into plain constants, branching with a plain
if— which produces a component that renders once correctly and never changes. - Effects that write signals other effects read, producing an update order that is defined but not obvious.
- A coarse signal holding a large object, which collapses fine granularity back to something like component-level updates.
- Subscriptions retained by a scope that outlives the view, which holds the computation and everything it closed over.
- Updates are batched within a task, so several signal writes produce one DOM pass and intermediate states are never painted.
- Asynchronous data races are unchanged: a slower earlier request can resolve after a faster later one and write a stale value into a signal (Out-of-Order Responses).
- Interpolated text is escaped; the raw-HTML property is the same sink it is in every other framework in this module (Cross-Site Scripting).
- Fine-grained bindings mean untrusted data can reach an attribute directly. Binding a URL from data to an
hrefis an injection path escaping does not close (Sanitization and Trusted HTML). - A signal is client state: readable and writable by anyone with devtools, including values a component chose not to display (What the Frontend Is Responsible For in Auth).
- Server rendering runs setup on the server, so anything captured in module scope is shared across requests rather than per user (Server-Side Rendering).
- "Signals are React state with different syntax." React state triggers a component re-run; a signal notifies subscribers. Nothing about the update path is shared.
- "Fine-grained means faster." It means less JavaScript per update. If the interaction is dominated by DOM mutation, layout, or waiting for the network, the difference will not be visible (Measure Before Optimising).
- "No re-render means no performance work." The work moved: expensive derivations, coarse signals and unbounded update frequency are still yours to manage.
- "Props are just an object." Props are accessors in disguise; destructuring them performs the reads immediately and cuts the subscriptions.
- "Effects here are React effects." They subscribe to what they read and re-run on change, rather than running after a commit with a hand-written dependency list.
Measuring it, and what changes in the field
- The framework's inspector shows the reactive graph: which computations exist and what they subscribe to. That graph *is* the program in this model.
- The Performance panel shows an unusually flat profile under interaction — small scripting spikes and DOM work, with no large render phase to find (A Mental Model of the Devtools).
- When something does not update, the measurement is the subscription: check whether the read happened in a tracking scope, because that is nearly always the answer.
- On a slow device the model is at its strongest, because what it removes is per-update JavaScript.
- On very high update frequencies it shines and can also outrun the display: updating a value faster than the browser paints is wasted work, and batching remains worthwhile (The Frame Budget).
- On a very large view tree, the memory held by subscriptions grows with the number of bindings — the cost this model pays instead of update time.
- On a small application with rare updates, the difference from any other model is not observable by a user.
- Near-minimal update work is bought with a stricter contract about how values are read. The compiler and runtime cannot rescue a read that happened outside a tracking scope.
- The mental model is small and unusual. Engineers arriving from a re-render model have to unlearn the habit of thinking of the component body as something that runs repeatedly.
- The ecosystem is the smallest of the five here, which is a real cost in libraries, examples, hiring and answers to unusual questions (Choosing a Framework).
Where this applies
Frontend advice ages badly and fragments across engines. These labels say what each claim is specific to, and where a different browser, device or framework would differ.
- FRAMEWORK-SPECIFICRunning the component function exactly once is Solid's defining choice. React re-runs it on every update, Vue re-runs it when a tracked dependency changes, Svelte compiles it away into create and update functions, and Angular keeps a persistent component instance and checks its bindings. Advice about memoizing components is meaningless here.
- SIMPLIFIEDDescribed as signals plus computations, which is the core. Omitted: the compile step that turns markup into node creation, the scheduler that batches and orders propagation, and the resource and transition primitives used for asynchronous data.
Where the depth lives
This domain teaches the browser-side mechanism and hands the rest off.
- — Programming Languages & Runtime Internals — automatic dependency tracking is dynamic scoping of a "current computation" during a read, and glitch-free propagation is a topological ordering problem over the resulting graph.