AI Agents · Solidjs

SolidJS Fine-Grained Reactivity

How SolidJS's signals-based fine-grained reactivity works under the hood, why it needs no virtual DOM, and how that changes the mental model compared to React's component re-render cycle.

John Kihiu12 min read

React re-renders a component function and diffs the result against a virtual DOM to figure out what changed. SolidJS skips both steps: it compiles your JSX into real DOM-manipulation code up front, and wires each piece of dynamic content directly to the signal that produces it. When a signal updates, exactly the DOM nodes that depend on it change — nothing re-runs, nothing gets diffed. That's fine-grained reactivity, and it's a different enough mental model from React that "it's like React but faster" undersells what's actually going on.

Signals are the primitive everything else is built on

A signal is a getter/setter pair that tracks who reads it. createSignal returns `[get, set]`; calling the getter inside a reactive scope (a JSX expression, an effect, a memo) registers that scope as a dependent. When you call the setter, only the registered dependents re-run — not the whole component function. Unlike React state, calling a signal's setter does not cause the "component" to execute again, because in Solid, component functions only run once, at creation, to build the DOM structure.

JSX · SOLIDJS SIGNALS
import { createSignal, createEffect, createMemo } from "solid-js";

function Counter() {
  const [count, setCount] = createSignal(0);
  const doubled = createMemo(() => count() * 2);

  createEffect(() => {
    console.log("count changed to", count());
  });

  return (
    
  );
}

Notice the function body of Counter runs exactly once. The `{count()}` and `{doubled()}` expressions inside the JSX are compiled into small reactive computations tied directly to the `

const [firstName, setFirstName] = createSignal("Ada");
const [lastName, setLastName] = createSignal("Lovelace");

// Recomputes only when firstName or lastName changes, never on unrelated updates
const fullName = createMemo(() => `${firstName()} ${lastName()}`);

createEffect(() => {
  document.title = fullName();
});
Reading a signal outside a tracked scope does nothing wrong, but tracks nothing

If you read count() in a plain function that isn't inside JSX, an effect, or a memo, Solid still gives you the current value — it just doesn't register a dependency, so nothing will automatically update when it changes later. This trips up developers moving from React, where reading state is always "safe" in this sense because everything re-renders anyway.

Why this changes how you write components

Because component functions run once, patterns that rely on React re-running the function body on every render don't apply the same way in Solid. Destructuring props at the top of a component breaks reactivity, because you've read the signal once outside any tracked context — Solid idioms keep props as accessors (`props.value()` inside JSX, not `const { value } = props`) specifically so the reactive graph can keep tracking them. This is the single most common mistake in React-to-Solid migrations, and it isn't a Solid quirk so much as a direct consequence of there being no re-render to fall back on.

Don't destructure props

function Greeting({ name }) { return <p>{name}</p> } captures `name`'s value once and never updates it, because destructuring reads the prop outside a tracked scope. Use function Greeting(props) { return <p>{props.name}</p> } instead — the JSX expression reads `props.name` inside a tracked context every time it needs to.

Wrapping up

React's model — re-render the component, diff the output, patch the DOM — trades some runtime cost for a very simple mental rule: state changes, function reruns, UI reflects the latest render. Solid trades that simplicity for direct wiring between signals and DOM nodes, verified and compiled ahead of time, so updates skip re-rendering and diffing altogether. The performance win is real, but the bigger shift is conceptual: in Solid, the component function is a setup script that runs once, not a template that reruns on every state change, and code that assumes otherwise (like destructured props) is the most common source of bugs when picking it up.

John Kihiu
Acumatica ERP Developer · Laravel Engineer

Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.