Getting started
Installation
# Vite template (recommended)
npx degit solidjs/templates/ts my-app
cd my-app
npm install
npm run dev
# TypeScript starter
npm create vite@latest my-solid-app -- --template solid-ts
# JavaScript starter
npm create vite@latest my-solid-app -- --template solid
Hello World
import { render } from "solid-js/web";
function App() {
return <h1>Hello, SolidJS!</h1>;
}
render(() => <App />, document.getElementById("root"));
Key Concepts
Components run once
- No re-renders
- Functions called once
- Only reactive expressions re-execute
Fine-grained reactivity
- Direct DOM updates
- No Virtual DOM diffing
- ~7KB minified + gzipped
Components
Function Components
// Components are factory functions
function Counter() {
const [count, setCount] = createSignal(0);
// This only runs ONCE
console.log("Component initialized");
return (
<button onClick={() => setCount(count() + 1)}>
Count: {count()} {/* Re-evaluates on change */}
</button>
);
}
Props
import { splitProps } from "solid-js";
function Greeting(props) {
// Props are read-only proxies
return <h1>Hello {props.name}!</h1>;
}
// With default values
function Button(props) {
const merged = mergeProps({ type: "button" }, props);
return <button type={merged.type}>{merged.children}</button>;
}
// Split props (useful for native elements)
function Input(props) {
const [local, others] = splitProps(props, ["label"]);
return (
<label>
{local.label}
<input {...others} />
</label>
);
}
Children
import { children } from "solid-js";
function Parent(props) {
// Resolve children (can be signals)
const c = children(() => props.children);
return (
<div>
{c()} {/* Call to access resolved children */}
</div>
);
}
Reactivity
createSignal
import { createSignal } from "solid-js";
const [count, setCount] = createSignal(0);
count(); // Read: returns 0
setCount(1); // Write: set to 1
setCount((c) => c + 1); // Update: increment
// With type
const [name, setName] = createSignal < string > "";
// With equality check
const [data, setData] = createSignal(obj, {
equals: false, // Always update (skip equality)
});
createEffect
import { createEffect, on } from "solid-js";
// Runs when dependencies change
createEffect(() => {
console.log("Count:", count());
});
// Explicit dependency tracking
createEffect(
on(count, (value) => {
console.log("Count changed to:", value);
}),
);
// With defer (skip initial run)
createEffect(
on(
count,
() => {
console.log("Only on updates");
},
{ defer: true },
),
);
// Multiple dependencies
createEffect(
on([count, name], ([c, n]) => {
console.log(`${n}: ${c}`);
}),
);
createMemo
import { createMemo } from "solid-js";
const [count, setCount] = createSignal(0);
// Cached computation
const doubled = createMemo(() => count() * 2);
doubled(); // Read like a signal
// Memos only recompute when dependencies change
const expensive = createMemo(() => {
console.log("Computing...");
return heavyCalculation(count());
});
Batching Updates
import { batch } from "solid-js";
// Multiple updates trigger one effect
batch(() => {
setCount(count() + 1);
setName("John");
setAge(30);
}); // Effects run once after batch
untrack
import { untrack } from "solid-js";
createEffect(() => {
console.log("Count:", count());
// Read name without tracking
const n = untrack(() => name());
console.log("Name:", n); // Won't re-run on name change
});
State Management
createStore
import { createStore } from "solid-js/store";
const [state, setState] = createStore({
user: { name: "John", age: 30 },
todos: [],
});
// Read (proxied)
state.user.name; // 'John'
// Update nested
setState("user", "name", "Jane");
setState("user", { age: 31 });
// Array operations
setState("todos", (todos) => [...todos, newTodo]);
setState("todos", 0, "completed", true);
// Function form
setState((store) => ({ ...store, loading: false }));
produce
import { produce } from "solid-js/store";
const [state, setState] = createStore({
todos: [{ text: "Learn Solid", done: false }],
});
// Mutable-style updates (Immer-like)
setState(
produce((s) => {
s.todos[0].done = true;
s.todos.push({ text: "Build app", done: false });
}),
);
reconcile
import { reconcile } from "solid-js/store";
const [state, setState] = createStore({
items: [],
});
// Efficiently merge new data
fetch("/api/items")
.then((r) => r.json())
.then((data) => {
setState("items", reconcile(data));
});
// With key (for arrays)
setState("users", reconcile(newUsers, { key: "id" }));
Nested Reactivity
const [state, setState] = createStore({
user: {
profile: {
name: "John",
},
},
});
// Fine-grained updates
createEffect(() => {
// Only runs when name changes
console.log(state.user.profile.name);
});
setState("user", "profile", "name", "Jane");
Control Flow
Show
import { Show } from 'solid-js';
// Conditional rendering
<Show when={loggedIn()}>
<Dashboard />
</Show>
// With fallback
<Show
when={user()}
fallback={<Login />}
>
<Profile user={user()} />
</Show>
// Non-keyed (preserves DOM)
<Show when={isEditing()} keyed={false}>
<Editor />
</Show>
For
import { For } from 'solid-js';
// Keyed by reference
<For each={items()}>
{(item, i) => (
<li>{i() + 1}. {item.name}</li>
)}
</For>
// With fallback
<For each={items()} fallback={<div>No items</div>}>
{item => <Item data={item} />}
</For>
// ⚠️ Index is a signal!
<For each={list()}>{(item, index) => <div>Position: {index()}</div>}</For>
Index
import { Index } from "solid-js";
// Keyed by index (better for primitives)
<Index each={colors()}>
{(color, i) => (
<div style={{ color: color() }}>
{i}: {color()}
</div>
)}
</Index>;
// ⚠️ Item is a signal!
<Index each={numbers()}>{(num) => <div>{num()}</div>}</Index>
Switch / Match
import { Switch, Match } from 'solid-js';
<Switch fallback={<NotFound />}>
<Match when={state.route === 'home'}>
<Home />
</Match>
<Match when={state.route === 'about'}>
<About />
</Match>
<Match when={state.route === 'contact'}>
<Contact />
</Match>
</Switch>
// With values
<Switch>
<Match when={user()?.role === 'admin'}>
<AdminPanel />
</Match>
<Match when={user()?.role === 'user'}>
<UserPanel />
</Match>
</Switch>
ErrorBoundary
import { ErrorBoundary } from "solid-js";
<ErrorBoundary
fallback={(err, reset) => (
<div>
Error: {err.message}
<button onClick={reset}>Try again</button>
</div>
)}
>
<MaybeErrorComponent />
</ErrorBoundary>;
Lifecycle
onMount
import { onMount } from "solid-js";
function Chart() {
let canvas;
onMount(() => {
// Runs after DOM insertion
const ctx = canvas.getContext("2d");
drawChart(ctx);
});
return <canvas ref={canvas} />;
}
onCleanup
import { onCleanup } from "solid-js";
function Timer() {
const [count, setCount] = createSignal(0);
const timer = setInterval(() => {
setCount((c) => c + 1);
}, 1000);
onCleanup(() => {
// Runs when component is removed
clearInterval(timer);
});
return <div>{count()}</div>;
}
Effect Cleanup
createEffect(() => {
const subscription = subscribe(topic());
onCleanup(() => {
subscription.unsubscribe();
});
});
Context
createContext
import { createContext, useContext } from "solid-js";
// Create context
const CounterContext = createContext();
// Provider
function CounterProvider(props) {
const [count, setCount] = createSignal(0);
const increment = () => setCount((c) => c + 1);
return (
<CounterContext.Provider value={{ count, increment }}>
{props.children}
</CounterContext.Provider>
);
}
// Consumer
function Counter() {
const { count, increment } = useContext(CounterContext);
return <button onClick={increment}>{count()}</button>;
}
With Default Value
const ThemeContext = createContext("light");
function ThemedButton() {
const theme = useContext(ThemeContext);
return <button class={theme}>Click</button>;
}
// Without provider, uses 'light'
Nested Contexts
function App() {
return (
<UserProvider>
<ThemeProvider>
<RouterProvider>
<Content />
</RouterProvider>
</ThemeProvider>
</UserProvider>
);
}
Resources
createResource
import { createResource } from "solid-js";
// Simple fetch
const [data] = createResource(fetchUser);
// With source (refetches when source changes)
const [userId, setUserId] = createSignal(1);
const [user] = createResource(userId, fetchUser);
// Full API
const [user, { mutate, refetch }] = createResource(userId, async (id) => {
const res = await fetch(`/api/users/${id}`);
return res.json();
});
Resource State
const [data] = createResource(fetcher);
data(); // Data or undefined
data.loading; // Boolean
data.error; // Error object
data.state; // 'unresolved' | 'pending' | 'ready' | 'refreshing' | 'errored'
data.latest; // Latest value (persists during refetch)
Suspense
import { Suspense } from 'solid-js';
<Suspense fallback={<Spinner />}>
<UserProfile /> {/* Has createResource */}
<UserPosts /> {/* Has createResource */}
</Suspense>
// Nested suspense
<Suspense fallback={<PageLoader />}>
<Header />
<Suspense fallback={<ContentLoader />}>
<Content />
</Suspense>
</Suspense>
SuspenseList
import { SuspenseList } from "solid-js";
// Coordinate multiple suspense boundaries
<SuspenseList revealOrder="forwards" tail="collapsed">
<Suspense fallback={<h2>Loading...</h2>}>
<ProfileDetails />
</Suspense>
<Suspense fallback={<h2>Loading...</h2>}>
<ProfileTimeline />
</Suspense>
<Suspense fallback={<h2>Loading...</h2>}>
<ProfileTrivia />
</Suspense>
</SuspenseList>;
Routing
Basic Router Setup
npm install @solidjs/router
import { Router, Route } from "@solidjs/router";
function App() {
return (
<Router>
<Route path="/" component={Home} />
<Route path="/about" component={About} />
<Route path="/users/:id" component={User} />
</Router>
);
}
Navigation
import { A, useNavigate, useParams } from '@solidjs/router';
// Link component
<A href="/about">About</A>
<A href="/users/1" activeClass="active">User 1</A>
// Programmatic navigation
function LoginButton() {
const navigate = useNavigate();
const handleLogin = async () => {
await login();
navigate('/dashboard');
};
return <button onClick={handleLogin}>Login</button>;
}
// Route params
function User() {
const params = useParams();
return <h1>User {params.id}</h1>;
}
Nested Routes
import { Outlet } from "@solidjs/router";
<Route path="/users" component={UsersLayout}>
<Route path="/" component={UsersList} />
<Route path="/:id" component={UserProfile} />
</Route>;
// UsersLayout.jsx
function UsersLayout() {
return (
<div>
<h1>Users</h1>
<Outlet /> {/* Nested routes render here */}
</div>
);
}
Route Data
import { useRouteData } from "@solidjs/router";
// Define data loader
function userDataLoader({ params }) {
const [user] = createResource(() => params.id, fetchUser);
return user;
}
// Route with data
<Route path="/users/:id" component={UserProfile} data={userDataLoader} />;
// Access in component
function UserProfile() {
const user = useRouteData();
return <Show when={user()}>{(u) => <h1>{u.name}</h1>}</Show>;
}
vs React
Reactivity Model
| React | SolidJS |
|---|---|
| Virtual DOM diffing | Direct DOM updates |
| Components re-render | Components run once |
useState returns value |
createSignal returns function |
useEffect on every render |
createEffect on dependency change |
Dependency arrays [deps] |
Automatic tracking |
Component Execution
// React: Function runs on every render
function ReactCounter() {
const [count, setCount] = useState(0);
console.log("Render!"); // Logs on every update
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
// Solid: Function runs ONCE
function SolidCounter() {
const [count, setCount] = createSignal(0);
console.log("Init!"); // Logs once
return <button onClick={() => setCount(count() + 1)}>{count()}</button>;
}
Derived State
// React
const doubled = count * 2; // Recalculates every render
// React (memoized)
const doubled = useMemo(() => count * 2, [count]);
// Solid (automatic)
const doubled = () => count() * 2;
// Solid (cached)
const doubled = createMemo(() => count() * 2);
Key Differences
// Signal reads are function calls
count(); // Solid
count; // React
// Signal writes
setCount(5); // Both
setCount((c) => c + 1); // Both
// Props are read-only proxies
props.value; // Solid (tracked)
const { value } = props; // ⚠️ Loses reactivity in Solid
// Control flow
{
items.map((i) => <Item />);
} // React
<For each={items()}>{(i) => <Item />}</For>; // Solid
Effects
// React
useEffect(() => {
console.log(count);
}, [count]); // Manual dependency
// Solid
createEffect(() => {
console.log(count()); // Auto-tracked
});
Performance
| Metric | React | SolidJS |
|---|---|---|
| Bundle size | ~42KB | ~7KB |
| Initial render | Fast | Faster |
| Updates | Virtual DOM | Direct mutations |
| Re-render scope | Component tree | Changed nodes only |
| Memory | Higher | Lower |
Common Patterns
Custom Hooks
function useCounter(initial = 0) {
const [count, setCount] = createSignal(initial);
const increment = () => setCount((c) => c + 1);
const decrement = () => setCount((c) => c - 1);
return { count, increment, decrement };
}
// Usage
function Counter() {
const counter = useCounter(10);
return (
<div>
<button onClick={counter.decrement}>-</button>
<span>{counter.count()}</span>
<button onClick={counter.increment}>+</button>
</div>
);
}
Refs
// DOM ref
function Input() {
let inputRef;
const focus = () => inputRef.focus();
return (
<>
<input ref={inputRef} />
<button onClick={focus}>Focus</button>
</>
);
}
// Callback ref
function Measured() {
const [size, setSize] = createSignal(null);
const measure = (el) => {
setSize(el.getBoundingClientRect());
};
return <div ref={measure}>Measure me</div>;
}
Portals
import { Portal } from "solid-js/web";
function Modal(props) {
return (
<Portal mount={document.getElementById("modal-root")}>
<div class="modal">{props.children}</div>
</Portal>
);
}
Dynamic Components
import { Dynamic } from "solid-js/web";
function DynamicComponent(props) {
return <Dynamic component={props.component} {...props.componentProps} />;
}
// Usage
<DynamicComponent component={isButton() ? Button : Link} href="/home" />;
Event Handling
// Synthetic events (delegated)
<button onClick={(e) => console.log(e)}>Click</button>
// Native events (not delegated)
<button on:click={(e) => console.log(e)}>Click</button>
// With event modifiers
<input
onKeyDown={(e) => {
if (e.key === 'Enter') submit();
}}
/>
Class and Style
// Class binding
<div class="static" classList={{
active: isActive(),
disabled: isDisabled()
}}>
Content
</div>
// Style binding
<div style={{
color: color(),
'font-size': `${size()}px`
}}>
Styled
</div>
// CSS variables
<div style={{ '--theme-color': themeColor() }}>
Content
</div>
Gotchas
Destructuring Props
// ❌ Loses reactivity
function Bad({ count }) {
return <div>{count}</div>; // Won't update!
}
// ✅ Access via props object
function Good(props) {
return <div>{props.count}</div>;
}
// ✅ Or use splitProps
function Better(props) {
const [local] = splitProps(props, ["count"]);
return <div>{local.count}</div>;
}
Calling Signals
const [count, setCount] = createSignal(0);
// ❌ Wrong
<div>{count}</div>; // Renders function object
setCount(count() + 1); // ✅ Correct read
// ✅ Correct
<div>{count()}</div>; // Call to read
setCount((c) => c + 1); // Callback form preferred
For vs Index
const [items, setItems] = createSignal(['A', 'B', 'C']);
// ✅ For: Reference-keyed (items are values)
<For each={items()}>
{(item) => <div>{item}</div>} // item is value
</For>
// ✅ Index: Index-keyed (items are signals)
<Index each={items()}>
{(item) => <div>{item()}</div>} // item() is value
</Index>
Early Returns
// ❌ Breaks reactivity
function Bad() {
const data = useContext(MyContext);
if (!data) return null; // Hooks must run!
return <div>{data.value}</div>;
}
// ✅ Use Show component
function Good() {
const data = useContext(MyContext);
return <Show when={data}>{(d) => <div>{d.value}</div>}</Show>;
}
createEffect vs createMemo
// ❌ Effect returns nothing
const value = createEffect(() => count() * 2); // undefined!
// ✅ Memo returns cached value
const value = createMemo(() => count() * 2);
console.log(value()); // Computed value
// Effect is for side effects only
createEffect(() => {
console.log("Count changed:", count());
});
Array Mutations
const [list, setList] = createSignal([1, 2, 3]);
// ❌ Direct mutation doesn't trigger
list().push(4); // Won't update UI
// ✅ Return new array
setList([...list(), 4]);
setList((l) => [...l, 4]);
TypeScript
Component Types
import { Component, JSX } from 'solid-js';
// Functional component
const Button: Component<{ label: string }> = (props) => {
return <button>{props.label}</button>;
};
// With children
type CardProps = {
title: string;
children: JSX.Element;
};
const Card: Component<CardProps> = (props) => {
return (
<div>
<h2>{props.title}</h2>
{props.children}
</div>
);
};
Signal Types
// Explicit type
const [count, setCount] = createSignal<number>(0);
const [user, setUser] = createSignal<User | null>(null);
// Union types
const [status, setStatus] = createSignal<"idle" | "loading" | "success">(
"idle",
);
// Inferred from initial value
const [name, setName] = createSignal(""); // string
Store Types
interface State {
user: { name: string; age: number };
todos: Todo[];
}
const [state, setState] = createStore<State>({
user: { name: "John", age: 30 },
todos: [],
});
Context Types
type ThemeContextType = {
theme: Accessor<"light" | "dark">;
setTheme: Setter<"light" | "dark">;
};
const ThemeContext = createContext<ThemeContextType>();
// With default value
const ThemeContext = createContext<ThemeContextType>({
theme: () => "light",
setTheme: () => {},
});
Testing
Setup (Vitest)
npm install -D vitest @solidjs/testing-library
import { render } from '@solidjs/testing-library';
import { describe, it, expect } from 'vitest';
describe('Counter', () => {
it('increments on click', async () => {
const { getByText } = render(() => <Counter />);
const button = getByText('Count: 0');
button.click();
expect(button.textContent).toBe('Count: 1');
});
});
Testing Signals
import { createRoot } from "solid-js";
it("updates signal", () => {
createRoot((dispose) => {
const [count, setCount] = createSignal(0);
expect(count()).toBe(0);
setCount(1);
expect(count()).toBe(1);
dispose();
});
});
Testing Effects
it("runs effect", () => {
createRoot((dispose) => {
const [count, setCount] = createSignal(0);
let effectRuns = 0;
createEffect(() => {
count(); // Track
effectRuns++;
});
expect(effectRuns).toBe(1);
setCount(1);
expect(effectRuns).toBe(2);
dispose();
});
});
Performance
Lazy Loading
import { lazy } from "solid-js";
// Code-split component
const HeavyComponent = lazy(() => import("./Heavy"));
function App() {
return (
<Suspense fallback={<Loading />}>
<HeavyComponent />
</Suspense>
);
}
Observable
import { observable } from "solid-js";
const [state] = createStore({ count: 0 });
// Use with external reactive libraries
const o = observable(state);
from
import { from } from "solid-js";
// Convert external observable to signal
const count = from(rxjsObservable$);
return <div>{count()}</div>;
Benchmarks
// SolidJS excels at:
// - Frequent updates to large lists
// - Deep component trees
// - Fine-grained state updates
// - Low memory overhead
// Benchmark results (js-framework-benchmark):
// - 1.5x faster than React
// - 3x faster than Vue 3
// - Comparable to Svelte
Also see
- Official Documentation (docs.solidjs.com)
- SolidJS Tutorial (solidjs.com)
- SolidJS Playground (playground.solidjs.com)
- Solid Router Docs (github.com)
- Awesome Solid (github.com)
- SolidJS vs React (solidjs.com)
- Performance Benchmarks (krausest.github.io)