solidjs
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
More from mgd34msu/goodvibes-gemini
chakra-ui
Builds accessible React applications with Chakra UI v3 components, tokens, and recipes. Use when creating styled component systems, theming, or accessible form controls.
70fastify
Builds high-performance Node.js APIs with Fastify, TypeScript, schema validation, and plugins. Use when building fast REST APIs, microservices, or needing schema-based validation.
2code-smell-detector
Detects code smells, anti-patterns, and common bugs with quantified thresholds and severity scoring. Use when reviewing code quality, finding maintainability issues, detecting SOLID violations, or identifying technical debt.
2playwright
Tests web applications with Playwright including E2E tests, locators, assertions, and visual testing. Use when writing end-to-end tests, testing across browsers, automating user flows, or debugging test failures.
2vitest
Tests JavaScript and TypeScript applications with Vitest including unit tests, mocking, coverage, and React component testing. Use when writing tests, setting up test infrastructure, mocking dependencies, or measuring code coverage.
2vite
Builds web applications with Vite including dev server, production builds, plugins, and configuration. Use when scaffolding projects, configuring build tools, optimizing bundles, or setting up development environments.
2