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.
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 `
No virtual DOM, because there's nothing to diff
React needs a virtual DOM because a component re-render produces a new tree of React elements that has to be compared against the previous tree to compute a minimal set of real DOM updates — the diffing is the cost of re-running the whole function on every state change. Solid's compiler already knows, at build time, exactly which DOM nodes are static and which are dynamic bindings tied to specific signals. There's no tree to regenerate and nothing to diff, because the "render" only happens once; updates after that are direct, targeted mutations. This is why Solid consistently shows up near the top of framework benchmarks for update performance — it isn't doing a faster version of React's work, it's skipping the category of work entirely.
createEffect and createMemo: derived and side-effecting reactivity
createMemo caches a derived computation and only recomputes when its signal dependencies change, similar in spirit to `useMemo` but automatically dependency-tracked — no dependency array to get wrong. createEffect runs a side effect whenever its tracked signals change, and (crucially) runs once immediately on creation to establish its dependencies, unlike `useEffect`'s dependency-array model.
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();
});
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.
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.
Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.