Solid.js takes a different approach to UI: instead of a virtual DOM, it compiles JSX directly to DOM operations. Instead of re-rendering components, it uses fine-grained reactivity where only the specific expressions that depend on changed data update. The result is typically faster performance with a smaller runtime.
Core concepts
Solid is built on three reactive primitives:
| Primitive | Purpose |
|---|---|
createSignal |
Reactive state |
createEffect |
Side effects on change |
createMemo |
Computed values |
These form the foundation of Solid's reactivity system.
Signals
Signals hold reactive state. Unlike useState, they return a getter function and a setter:
import { createSignal } from 'solid-js';
function Counter() {
const [count, setCount] = createSignal(0);
return (
<button onClick={() => setCount(count() + 1)}>
Count: {count()}
</button>
);
}
Key differences from React:
countis a function—call it to read the value- The component function runs once, not on every update
- Only
{count()}updates when the signal changes
Fine-grained updates
In React, state changes re-render the component. In Solid, only reactive expressions re-run:
function Profile() {
const [name, setName] = createSignal('Alice');
const [age, setAge] = createSignal(30);
console.log('Component runs once');
return (
<div>
<p>Name: {name()}</p> {/* Updates when name changes */}
<p>Age: {age()}</p> {/* Updates when age changes */}
</div>
);
}
The console log runs once. Changing name only updates the first paragraph's text node.
Effects
createEffect runs side effects when dependencies change:
import { createSignal, createEffect } from 'solid-js';
function Timer() {
const [count, setCount] = createSignal(0);
createEffect(() => {
console.log('Count changed:', count());
// Automatically tracks count() as a dependency
});
return <button onClick={() => setCount(c => c + 1)}>{count()}</button>;
}
No dependency array needed—Solid tracks what you read.
Cleanup
Return a cleanup function from effects:
createEffect(() => {
const handler = () => console.log('scroll');
window.addEventListener('scroll', handler);
return () => window.removeEventListener('scroll', handler);
});
Memos
createMemo caches computed values:
import { createSignal, createMemo } from 'solid-js';
function FilteredList() {
const [items, setItems] = createSignal([1, 2, 3, 4, 5]);
const [filter, setFilter] = createSignal(2);
const filtered = createMemo(() =>
items().filter(item => item > filter())
);
return (
<ul>
<For each={filtered()}>
{(item) => <li>{item}</li>}
</For>
</ul>
);
}
The memo recalculates only when items or filter change.
Control flow
Solid uses components for control flow instead of JavaScript:
import { Show, For, Switch, Match } from 'solid-js';
function App() {
const [show, setShow] = createSignal(true);
const [items, setItems] = createSignal(['a', 'b', 'c']);
const [status, setStatus] = createSignal('loading');
return (
<>
{/* Conditional */}
<Show when={show()} fallback={<p>Hidden</p>}>
<p>Visible</p>
</Show>
{/* List */}
<For each={items()}>
{(item, index) => <li>{index()}: {item}</li>}
</For>
{/* Switch/case */}
<Switch>
<Match when={status() === 'loading'}>Loading...</Match>
<Match when={status() === 'error'}>Error!</Match>
<Match when={status() === 'success'}>Done!</Match>
</Switch>
</>
);
}
These components enable fine-grained updates—items aren't recreated when the list changes.
Stores for complex state
For nested reactive data, use stores:
import { createStore } from 'solid-js/store';
function TodoApp() {
const [state, setState] = createStore({
todos: [
{ id: 1, text: 'Learn Solid', done: false },
{ id: 2, text: 'Build app', done: false },
],
});
const toggle = (id: number) => {
setState('todos', todo => todo.id === id, 'done', done => !done);
};
return (
<For each={state.todos}>
{(todo) => (
<div onClick={() => toggle(todo.id)}>
{todo.text}: {todo.done ? '✓' : '○'}
</div>
)}
</For>
);
}
Stores provide path-based updates for nested data.
Comparison with React
| Aspect | React | Solid |
|---|---|---|
| Rendering | Virtual DOM diffing | Direct DOM updates |
| State | useState returns value |
createSignal returns getter |
| Re-renders | Whole component | Specific expressions |
| Hooks rules | Order matters | No rules |
| Runtime size | ~40KB | ~7KB |
| Performance | Good with optimization | Fast by default |
JSX differences
Solid JSX looks like React but has differences:
// Solid JSX
function Button(props) {
return (
<button
class={props.class} // class, not className
classList={{ active: props.active }} // dynamic classes
style={{ color: props.color }} // object required
onClick={props.onClick} // not onClickCapture
ref={props.ref} // ref as prop
>
{props.children}
</button>
);
}
Props are getters
Props in Solid are getters to preserve reactivity:
// DON'T destructure at top level
function Bad({ name }) { // Loses reactivity!
return <p>{name}</p>;
}
// DO access through props
function Good(props) {
return <p>{props.name}</p>;
}
// OR use splitProps for partial destructuring
function Better(props) {
const [local, rest] = splitProps(props, ['name']);
return <p {...rest}>{local.name}</p>;
}
SolidStart
SolidStart is Solid's meta-framework (like Next.js for React):
// routes/index.tsx
import { createResource } from 'solid-js';
export default function Home() {
const [data] = createResource(fetchData);
return (
<Show when={data()} fallback={<p>Loading...</p>}>
<main>{data().title}</main>
</Show>
);
}
Features include file-based routing, SSR, and streaming.
When to use Solid
| Use case | Recommendation |
|---|---|
| Performance-critical UI | Strong fit |
| Small bundle size needed | Strong fit |
| Embedded widgets | Strong fit |
| Large existing React codebase | Likely not worth migrating |
| Team knows React only | Learning curve to consider |
| Need React ecosystem | Stick with React |
Migration considerations
Moving from React to Solid requires:
- Learning the reactivity model
- Rewriting components (similar but different)
- Finding Solid equivalents for React libraries
- Adjusting mental model (no re-renders)
For greenfield projects, Solid is worth evaluating. For existing React apps, the migration cost is often not justified.
Summary
Solid.js provides fine-grained reactivity without a virtual DOM. Signals track state, effects handle side effects, and memos cache computations. Components run once; only reactive expressions update. The result is typically faster performance with smaller bundles. Consider Solid for new projects where performance matters and you're willing to learn a different reactivity model.
