| name | solidjs |
| description | Builds UIs with SolidJS including signals, effects, memos, and fine-grained reactivity. Use when creating high-performance reactive applications, building without virtual DOM, or needing granular updates. |
SolidJS
A declarative JavaScript library for building UIs with fine-grained reactivity.
Quick Start
Create project:
npx degit solidjs/templates/ts my-app
cd my-app
npm install
npm run dev
Signals
Basic Signal
import { createSignal } from 'solid-js';
function Counter() {
const [count, setCount] = createSignal(0);
return (
<button onClick={() => setCount(count() + 1)}>
Count: {count()}
</button>
);
}
Signal Patterns
import { createSignal } from 'solid-js';
function App() {
// Primitive signal
const [name, setName] = createSignal('');
// Object signal
const [user, setUser] = createSignal({ name: 'John', age: 30 });
// Update primitives
setName('Jane');
// Update objects (replace entire object)
setUser({ ...user(), age: 31 });
// Functional update
setCount(prev => prev + 1);
return (
<div>
<p>Name: {name()}</p>
<p>User: {user().name}</p>
</div>
);
}
Effects
createEffect
import { createSignal, createEffect } from 'solid-js';
function Logger() {
const [count, setCount] = createSignal(0);
// Runs on initial render and when count changes
createEffect(() => {
console.log('Count changed:', count());
});
// With cleanup
createEffect(() => {
const handler = () => console.log('clicked');
document.addEventListener('click', handler);
// Cleanup function
onCleanup(() => {
document.removeEventListener('click', handler);
});
});
return <button onClick={() => setCount(c => c + 1)}>Increment</button>;
}
onMount and onCleanup
import { onMount, onCleanup } from 'solid-js';
function Timer() {
const [time, setTime] = createSignal(0);
onMount(() => {
const interval = setInterval(() => {
setTime(t => t + 1);
}, 1000);
onCleanup(() => clearInterval(interval));
});
return <p>Time: {time()}s</p>;
}
Memos (Derived State)
import { createSignal, createMemo } from 'solid-js';
function App() {
const [count, setCount] = createSignal(0);
const [multiplier, setMultiplier] = createSignal(2);
// Computed value - only recalculates when dependencies change
const doubled = createMemo(() => count() * multiplier());
// Expensive computation
const filtered = createMemo(() => {
console.log('Computing filtered list...');
return items().filter(item => item.active);
});
return (
<div>
<p>Count: {count()}</p>
<p>Doubled: {doubled()}</p>
</div>
);
}
Components
Basic Component
import { Component } from 'solid-js';
interface Props {
name: string;
age?: number;
}
const Greeting: Component<Props> = (props) => {
return (
<div>
<h1>Hello, {props.name}!</h1>
{props.age && <p>Age: {props.age}</p>}
</div>
);
};
// Usage
<Greeting name="John" age={30} />
Props with Defaults
import { mergeProps } from 'solid-js';
interface Props {
count?: number;
label?: string;
}
function Counter(props: Props) {
const merged = mergeProps({ count: 0, label: 'Count' }, props);
return (
<p>{merged.label}: {merged.count}</p>
);
}
Splitting Props
import { splitProps } from 'solid-js';
interface Props {
name: string;
class?: string;
style?: string;
}
function Button(props: Props) {
const [local, others] = splitProps(props, ['name']);
return (
<button {...others}>
{local.name}
</button>
);
}
Children
import { ParentComponent, children } from 'solid-js';
const Card: ParentComponent<{ title: string }> = (props) => {
return (
<div class="card">
<h2>{props.title}</h2>
<div class="content">
{props.children}
</div>
</div>
);
};
// With resolved children
const List: ParentComponent = (props) => {
const resolved = children(() => props.children);
createEffect(() => {
console.log('Children:', resolved());
});
return <ul>{resolved()}</ul>;
};
Control Flow
Show
import { Show } from 'solid-js';
function App() {
const [loggedIn, setLoggedIn] = createSignal(false);
return (
<Show
when={loggedIn()}
fallback={<button onClick={() => setLoggedIn(true)}>Log in</button>}
>
<button onClick={() => setLoggedIn(false)}>Log out</button>
</Show>
);
}
For
import { For } from 'solid-js';
function TodoList() {
const [todos, setTodos] = createSignal([
{ id: 1, text: 'Learn Solid' },
{ id: 2, text: 'Build app' },
]);
return (
<ul>
<For each={todos()}>
{(todo, index) => (
<li>
{index() + 1}. {todo.text}
</li>
)}
</For>
</ul>
);
}
Index
import { Index } from 'solid-js';
// For non-keyed lists where items may change but indices stay stable
function Grid() {
const [cells, setCells] = createSignal(['A', 'B', 'C']);
return (
<div>
<Index each={cells()}>
{(cell, index) => (
<div>{index}: {cell()}</div>
)}
</Index>
</div>
);
}
Switch/Match
import { Switch, Match } from 'solid-js';
function StatusMessage() {
const [status, setStatus] = createSignal('loading');
return (
<Switch fallback={<p>Unknown status</p>}>
<Match when={status() === 'loading'}>
<p>Loading...</p>
</Match>
<Match when={status() === 'success'}>
<p>Success!</p>
</Match>
<Match when={status() === 'error'}>
<p>Error occurred</p>
</Match>
</Switch>
);
}
Dynamic
import { Dynamic } from 'solid-js/web';
function App() {
const [component, setComponent] = createSignal('div');
return (
<Dynamic component={component()} class="container">
Content
</Dynamic>
);
}
Stores (Complex State)
import { createStore, produce } from 'solid-js/store';
interface Todo {
id: number;
text: string;
completed: boolean;
}
function TodoApp() {
const [todos, setTodos] = createStore<Todo[]>([]);
const addTodo = (text: string) => {
setTodos(todos.length, {
id: Date.now(),
text,
completed: false,
});
};
const toggleTodo = (id: number) => {
setTodos(
todo => todo.id === id,
'completed',
completed => !completed
);
};
// Using produce for complex updates
const removeTodo = (id: number) => {
setTodos(produce(todos => {
const index = todos.findIndex(t => t.id === id);
if (index !== -1) todos.splice(index, 1);
}));
};
return (
<For each={todos}>
{todo => (
<div>
<span style={{ 'text-decoration': todo.completed ? 'line-through' : 'none' }}>
{todo.text}
</span>
<button onClick={() => toggleTodo(todo.id)}>Toggle</button>
</div>
)}
</For>
);
}
Context
import { createContext, useContext, ParentComponent } from 'solid-js';
interface ThemeContext {
theme: () => string;
setTheme: (theme: string) => void;
}
const ThemeContext = createContext<ThemeContext>();
const ThemeProvider: ParentComponent = (props) => {
const [theme, setTheme] = createSignal('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{props.children}
</ThemeContext.Provider>
);
};
function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within ThemeProvider');
}
return context;
}
function ThemedButton() {
const { theme, setTheme } = useTheme();
return (
<button onClick={() => setTheme(theme() === 'light' ? 'dark' : 'light')}>
Theme: {theme()}
</button>
);
}
Resources (Async Data)
import { createResource, Suspense, ErrorBoundary } from 'solid-js';
async function fetchUser(id: string) {
const res = await fetch(`/api/users/${id}`);
return res.json();
}
function UserProfile() {
const [userId, setUserId] = createSignal('1');
const [user, { refetch, mutate }] = createResource(userId, fetchUser);
return (
<ErrorBoundary fallback={<p>Error loading user</p>}>
<Suspense fallback={<p>Loading...</p>}>
<Show when={user()}>
<div>
<h1>{user().name}</h1>
<button onClick={refetch}>Refresh</button>
</div>
</Show>
</Suspense>
</ErrorBoundary>
);
}
Refs
function TextInput() {
let inputRef: HTMLInputElement | undefined;
onMount(() => {
inputRef?.focus();
});
return <input ref={inputRef} />;
}
// Callback ref
function CallbackRef() {
return (
<input ref={(el) => {
// Called when element is created
el.focus();
}} />
);
}
Directives
// Define directive
function clickOutside(el: Element, accessor: () => () => void) {
const onClick = (e: MouseEvent) => {
if (!el.contains(e.target as Node)) {
accessor()?.();
}
};
document.addEventListener('click', onClick);
onCleanup(() => document.removeEventListener('click', onClick));
}
// Use directive
function Dropdown() {
const [open, setOpen] = createSignal(false);
return (
<div use:clickOutside={() => setOpen(false)}>
<button onClick={() => setOpen(true)}>Open</button>
<Show when={open()}>
<div>Dropdown content</div>
</Show>
</div>
);
}
Best Practices
- Call signals as functions - Always use
count()notcount - Use For for keyed lists - More efficient than map
- Use stores for complex state - Nested reactivity
- Avoid destructuring props - Breaks reactivity
- Use createMemo for expensive computations - Caches results
Common Mistakes
| Mistake | Fix |
|---|---|
| Destructuring props | Access props.x directly |
| Forgetting to call signal | Use count() not count |
| Using map instead of For | Use For component |
| Mutating signal objects | Replace entire object |
| Missing Suspense for resources | Wrap in Suspense |
Reference Files
- references/reactivity.md - Reactivity deep dive
- references/stores.md - Store patterns
- references/router.md - Solid Router