git:20260320.766fb9e to git:20260906.ae0cc61

103 added, 357 removed. Audit A to A.

---
name: web-framework-solidjs
- description: SolidJS fine-grained reactivity patterns - signals, effects, memos, stores, createResource, control flow components, Suspense, SolidStart
+ description: SolidJS fine-grained reactivity — signals, effects, memos, stores, resources, control-flow components. Load when writing Solid components or reactive state.
---
# SolidJS Patterns
- > **Quick Guide:** Use `createSignal` for primitives, `createStore` for nested objects. Always call signals as functions (`count()` not `count`). Never destructure props. Use `<Show>`, `<For>`, `<Switch>` for control flow. Wrap async data in `createResource` and components in `<Suspense>`.
+ > **Quick Guide:** A signal is read by calling it — `count()`, never `count` — and a component body runs once, so everything that must change over time is an expression inside JSX rather than a re-render. Props are a live proxy: destructuring them freezes their values. Conditionals and lists go through `<Show>`, `<For>`, `<Index>` and `<Switch>` so Solid can update the DOM node rather than the subtree.
- ---
+ **Detailed Resources:**
- <critical_requirements>
+ - [examples/core.md](examples/core.md) — signals, effects, `on()`, memos, `batch`
+ - [examples/components.md](examples/components.md) — `splitProps`, `mergeProps`, control flow, refs, component types, `<Dynamic>`
+ - [examples/stores.md](examples/stores.md) — `createStore`, `produce`, `reconcile`, context
+ - [examples/resources.md](examples/resources.md) — `createResource`, and `createAsync` + `query` under SolidStart
+ - [reference.md](reference.md) — decision trees, the anti-pattern routing table, import cheat sheet, review checklists
- ## CRITICAL: Before Using This Skill
+ ---
- > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants)
+ ## Which path applies
- **(You MUST call signals as functions to read values - `count()` NOT `count`)**
+ - **Plain SolidJS** — async data goes through `createResource`, read under `<Suspense>`. Follow [examples/resources.md](examples/resources.md) from the top.
+ - **SolidStart with `@solidjs/router`** — `createAsync` + `query` replace `createResource`, because they deduplicate and serialise across the server boundary. Same file, second half. Everything else on this page is unchanged.
- **(You MUST NEVER destructure props - use `props.name` or `splitProps()` to preserve reactivity)**
+ ---
- **(You MUST use `<Show>`, `<For>`, `<Switch>` control flow components instead of ternaries and `.map()`)**
+ <critical_requirements>
- **(You MUST clean up side effects with `onCleanup()` inside effects)**
+ ## Before writing SolidJS code
- **(You MUST wrap async data fetching in `createResource` and components in `<Suspense>`)**
+ **Call a signal to read it — `count()`.** The call is what subscribes the surrounding computation; the bare reference is just a function object, and nothing warns.
- </critical_requirements>
+ **Reach into props rather than destructuring them — `props.name`, or `splitProps()` when you need a subset.** Props are a getter proxy, so a destructured value is a snapshot taken once at creation.
- ---
+ **Express conditionals and lists with `<Show>`, `<For>`, `<Index>` and `<Switch>`.** These update the affected node; a ternary or `.map()` rebuilds the subtree because the component body will not run again to fix it.
- **Auto-detection:** SolidJS, createSignal, createEffect, createMemo, createStore, createResource, createAsync, query, action, Show, For, Switch, Match, splitProps, mergeProps, onCleanup, onMount, Suspense, ErrorBoundary, solid-js, @solidjs/router, SolidStart
+ **Register an `onCleanup()` beside anything an effect opens.** It runs before the next execution as well as on disposal, so one call covers both re-runs and unmount.
- **When to use:**
+ **Fetch through `createResource` (or `createAsync` under SolidStart) and read it under `<Suspense>`.** Both carry loading and error state and cancel superseded requests.
- - Building reactive UIs with fine-grained reactivity (no virtual DOM)
- - Managing state with signals (primitives) and stores (nested objects)
- - Creating derived values with memos
- - Fetching async data with createResource
- - Building full-stack apps with SolidStart
+ </critical_requirements>
- **Key patterns covered:**
+ ---
- - Signals, effects, and memos (core reactivity)
- - Component patterns (props, splitProps, mergeProps, refs)
- - Control flow components (Show, For, Index, Switch, Match)
- - Stores for complex nested state
- - createResource for async data fetching (plain SolidJS)
- - createAsync + query for data fetching (SolidStart, recommended for Solid 2.0)
- - Context for dependency injection
- - Suspense and ErrorBoundary for async handling
- - SolidStart patterns (server functions, query, actions)
+ **Auto-detection:** SolidJS, solid-js, createSignal, createEffect, createMemo, createStore, createResource, createAsync, splitProps, mergeProps, onCleanup, onMount, untrack, batch, produce, reconcile, Show, For, Index, Switch, Match, Dynamic, @solidjs/router, SolidStart
- **When NOT to use:**
+ **Applies to:**
- - When team is deeply invested in React ecosystem
- - Projects requiring extensive third-party React component libraries
- - When you need React-specific features (Server Components, concurrent mode)
+ - Signals, memos and effects, and which of the three a value belongs in
+ - Component props, refs, component types and polymorphic elements
+ - Control-flow components and list keying
+ - Stores for nested state, and context built on top of one
+ - Async data with `createResource`, `createAsync` and `query`
- **Detailed Resources:**
+ **Handled elsewhere:**
- - [examples/core.md](examples/core.md) - Signals, effects, memos, batch
- - [examples/components.md](examples/components.md) - Props handling, control flow, refs, component types
- - [examples/stores.md](examples/stores.md) - createStore, produce, reconcile, Context
- - [examples/resources.md](examples/resources.md) - createResource, createAsync, query/action (SolidStart)
- - [reference.md](reference.md) - Decision frameworks, anti-patterns, checklists
+ - Styling — components bind a `class` attribute and settle nothing about what fills it
+ - Client state libraries beyond signals and stores, and server-state caching layers
+ - Routing itself — this skill covers only the data primitives a route loads with
+ - Test doubles for the network
---
<philosophy>
## Philosophy
- SolidJS achieves exceptional performance through **fine-grained reactivity**: instead of re-rendering entire component trees like React, Solid tracks dependencies at the expression level and surgically updates only the specific DOM nodes that changed. Components run once during creation, not on every state change.
-
- **Core principles:**
-
- 1. **Fine-grained reactivity** - Updates happen at the DOM node level, not component level
- 2. **Signals are functions** - Reading a signal (`count()`) subscribes to it, creating automatic dependency tracking
- 3. **Components run once** - The component function body executes only at creation time
- 4. **No virtual DOM** - Direct DOM manipulation eliminates diffing overhead
- 5. **Explicit reactivity** - State is explicitly reactive via `createSignal` and `createStore`
-
- **Key mental model:**
+ Solid tracks dependencies at the expression level. A component function runs once, at creation; what re-runs afterwards is each reactive expression that read a changed signal, and what updates is the single DOM node that expression produced. There is no virtual DOM and no diff.
- ```typescript
- // React: Component re-renders, recalculates everything
- function Counter() {
- const [count, setCount] = useState(0);
- console.log('This runs on EVERY update'); // Re-runs
- return <span>{count}</span>; // Re-renders span
- }
+ Two consequences drive every pattern here. First, reading is subscribing: `count()` inside a computation registers a dependency, and the same read in an event handler does not, because handlers are outside any tracking scope. Second, anything captured as a plain value has left the graph — a destructured prop, a variable assigned from `store.field`, a value read before an `await` — so reactivity is lost silently rather than loudly.
- // Solid: Component runs once, only expressions update
- function Counter() {
- const [count, setCount] = createSignal(0);
- console.log('This runs ONCE'); // Only at creation
- return <span>{count()}</span>; // Only text node updates
- }
- ```
+ Three habits carried in from re-rendering frameworks have no counterpart here, and none of them needs a replacement. `createEffect` and `createMemo` take no dependency array — both discover what they read as they run it. There is nothing to memoise at the component level, because a component is never re-invoked and so there is no re-render to skip. And there is no rule about _where_ a primitive may be called — `createSignal` inside a branch or a loop is legal, since the primitives run at creation rather than on every render. What takes that rule's place is ownership: a primitive is disposed by the reactive owner it was created under, so one created outside any owner is never cleaned up.
</philosophy>
---
<patterns>
- ## Core Patterns
-
- ### Pattern 1: Signals - Reactive Primitives
+ ## Core patterns
- Signals are the foundation of Solid's reactivity. They hold a value and notify subscribers when it changes.
+ ### Pattern 1: Signals
- #### Basic Signals
+ `createSignal` returns a getter and a setter. The getter is called to read; the setter takes a value or a function of the previous one.
```typescript
import { createSignal } from "solid-js";
- const MAX_COUNT = 100;
- const INITIAL_COUNT = 0;
-
- // createSignal returns [getter, setter]
- const [count, setCount] = createSignal(INITIAL_COUNT);
-
- // MUST call as function to read
- console.log(count()); // 0
-
- // Setting values
- setCount(5);
- setCount((prev) => prev + 1); // Functional update
-
- // With TypeScript explicit types
+ const [count, setCount] = createSignal(0);
const [user, setUser] = createSignal<User | null>(null);
- ```
- **Why good:** Explicit reactivity through function calls, automatic dependency tracking, type-safe with generics, functional updates prevent stale closure bugs
-
- #### Signals in Components
-
- ```typescript
- import { createSignal, type Component } from 'solid-js';
-
- const Counter: Component = () => {
- const [count, setCount] = createSignal(0);
-
- // This console.log runs ONCE, not on every update
- console.log('Component created');
-
- return (
- <div>
- {/* Only this text node updates when count changes */}
- <span>Count: {count()}</span>
- <button onClick={() => setCount(c => c + 1)}>Increment</button>
- </div>
- );
- };
-
- export { Counter };
+ count(); // read — and subscribe, inside a tracking scope
+ setCount(5);
+ setCount((prev) => prev + 1);
```
- **Why good:** Component body runs once, only `{count()}` expression re-evaluates on update, minimal DOM manipulation
+ Full code: [examples/core.md](examples/core.md)
---
- ### Pattern 2: Effects - Side Effects on State Changes
-
- Effects run automatically when their tracked dependencies change.
+ ### Pattern 2: Effects
- #### createEffect
+ An effect re-runs when any signal it read changes; there is no dependency array. `onCleanup` inside it runs before each re-run and on disposal.
```typescript
- import { createSignal, createEffect, onCleanup } from "solid-js";
-
- const [count, setCount] = createSignal(0);
-
- // Automatically tracks count() as dependency
createEffect(() => {
- console.log("Count changed:", count());
- });
-
- // Effect with cleanup
- createEffect(() => {
- const handler = () => console.log("Clicked, count:", count());
+ const handler = () => report(count());
window.addEventListener("click", handler);
-
- // MUST clean up to prevent memory leaks
- onCleanup(() => {
- window.removeEventListener("click", handler);
- });
+ onCleanup(() => window.removeEventListener("click", handler));
});
```
- **Why good:** Automatic dependency tracking (no dependency array), onCleanup runs before each re-execution and on disposal
-
- #### Explicit Tracking with on()
+ `on()` narrows that to named dependencies and gives you the previous value, so reads inside the callback no longer subscribe.
```typescript
- import { createSignal, createEffect, on } from "solid-js";
-
- const [count, setCount] = createSignal(0);
- const [name, setName] = createSignal("");
-
- // Only tracks count, ignores name even if accessed
- createEffect(
- on(count, (value, prev) => {
- console.log("Count went from", prev, "to", value);
- // name() here won't add a dependency
- console.log("Current name:", name());
- }),
- );
-
- // Multiple explicit dependencies
- createEffect(
- on([count, name], ([c, n]) => {
- console.log("Either changed:", c, n);
- }),
- );
+ createEffect(on(count, (value, prev) => report(prev, value)));
```
- **Why good:** Explicit control over what triggers the effect, access to previous value
+ Full code: [examples/core.md](examples/core.md)
---
- ### Pattern 3: Memos - Cached Derived Values
-
- Memos cache computed values and only recalculate when dependencies change.
+ ### Pattern 3: Memos
- #### createMemo
+ `createMemo` caches a derived value and recomputes it only when a dependency changes. Memos chain, so a filter feeding a sort recomputes only the stage that was invalidated.
```typescript
- import { createSignal, createMemo } from "solid-js";
-
- const [items, setItems] = createSignal<Item[]>([]);
- const [filter, setFilter] = createSignal("");
-
- // Only recalculates when items or filter changes
- const filteredItems = createMemo(() => {
- console.log("Filtering..."); // Only runs when dependencies change
- return items().filter((item) =>
- item.name.toLowerCase().includes(filter().toLowerCase()),
- );
- });
-
- // Expensive computation - memoized automatically
- const sortedItems = createMemo(() => {
- return [...items()].sort((a, b) => a.name.localeCompare(b.name));
- });
+ const filtered = createMemo(() =>
+ items().filter((item) => item.name.includes(filter())),
+ );
+ const sorted = createMemo(() =>
+ [...filtered()].sort((a, b) => a.name.localeCompare(b.name)),
+ );
```
- **Why good:** Caches result until dependencies change, prevents unnecessary recalculations, clearer than inline expressions for complex logic
+ Full code: [examples/core.md](examples/core.md)
---
- ### Pattern 4: Component Props
-
- Never destructure props in Solid - it breaks reactivity.
+ ### Pattern 4: Props
- #### Props with splitProps and mergeProps
+ `mergeProps` applies defaults and `splitProps` separates your own props from the ones destined for a DOM element — both preserving the getters that destructuring would flatten.
```typescript
- import { splitProps, mergeProps, type Component, type JSX } from 'solid-js';
-
- interface ButtonProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement> {
- variant?: 'primary' | 'secondary';
- loading?: boolean;
- }
-
const Button: Component<ButtonProps> = (rawProps) => {
- // Default props with mergeProps
- const props = mergeProps({ variant: 'primary' as const }, rawProps);
-
- // Split custom props from native HTML attributes
- const [local, buttonProps] = splitProps(props, ['variant', 'loading']);
+ const props = mergeProps({ variant: "primary" as const }, rawProps);
+ const [local, buttonProps] = splitProps(props, ["variant", "loading"]);
- return (
- <button
- {...buttonProps}
- class={`btn btn-${local.variant}`}
- disabled={local.loading || buttonProps.disabled}
- >
- {local.loading ? 'Loading...' : props.children}
- </button>
- );
+ return <button {...buttonProps} data-variant={local.variant} disabled={local.loading} />;
};
-
- export { Button };
```
- **Why good:** splitProps separates custom props from spread-able HTML props, mergeProps provides defaults while preserving reactivity, never destructure props
-
- #### Component Types
-
- Use `VoidComponent` (no children), `ParentComponent` (children required), or `Component` (children optional) for type-safe children handling.
-
- ```typescript
- const Icon: VoidComponent<{ name: string }> = (props) => (/* ... */);
- const Card: ParentComponent<{ title: string }> = (props) => (/* ... */);
- ```
+ Type the component by what it does with children: `VoidComponent` refuses them, `ParentComponent` requires them, `Component` leaves them optional.
- See [examples/components.md](examples/components.md) for full component type examples.
+ Full code: [examples/components.md](examples/components.md)
---
- ### Pattern 5: Control Flow Components
-
- Solid uses dedicated components for control flow instead of JavaScript expressions.
-
- #### Show for Conditionals
+ ### Pattern 5: Control flow
```typescript
- import { Show } from 'solid-js';
-
- // Basic condition with fallback
<Show when={user()} fallback={<LoginForm />}>
- <Dashboard />
- </Show>
-
- // Keyed flow - access the truthy value safely
- <Show when={user()} fallback={<LoginForm />}>
{(user) => <Dashboard user={user()} />}
</Show>
- ```
- **Why good:** Optimized for fine-grained updates, keyed flow provides narrowed type
-
- #### For for Lists
-
- ```typescript
- import { For } from 'solid-js';
-
- // Basic list rendering
<For each={items()} fallback={<p>No items</p>}>
- {(item, index) => (
- <li>
- {index()}: {item.name}
- </li>
- )}
+ {(item, index) => <li>{index()}: {item.name}</li>}
</For>
- ```
- **Why good:** Automatically handles keying by reference, index() is a signal, optimized list diffing
-
- #### Switch/Match for Multiple Conditions
-
- ```typescript
- import { Switch, Match } from 'solid-js';
-
- <Switch fallback={<p>Unknown status</p>}>
- <Match when={status() === 'loading'}>
- <Spinner />
- </Match>
- <Match when={status() === 'error'}>
- <ErrorMessage error={error()} />
- </Match>
- <Match when={status() === 'success'}>
- <SuccessView data={data()} />
- </Match>
+ <Switch fallback={<p>Unknown</p>}>
+ <Match when={status() === "loading"}><Spinner /></Match>
+ <Match when={status() === "error"}><ErrorMessage error={error()} /></Match>
</Switch>
```
- **Why good:** First matching condition renders, cleaner than nested Shows
+ The callback form of `<Show>` narrows the value to non-null. `<For>` keys by item reference, so rows survive reordering; `<Index>` keys by position and hands each item as a signal, which suits a fixed-length list whose values change.
+ Full code: [examples/components.md](examples/components.md)
+
---
### Pattern 6: Refs
- Refs work differently in Solid - no `forwardRef` needed.
-
- #### DOM and Component Refs
+ A ref is an ordinary prop — assignment or a callback, and no wrapper component in either direction.
```typescript
- import { onMount, type Component } from 'solid-js';
-
- const Form: Component = () => {
- let inputRef: HTMLInputElement;
-
- onMount(() => {
- // Ref is available after mount
- inputRef.focus();
- });
+ let inputRef: HTMLInputElement;
- return (
- <form>
- {/* Ref callback or assignment */}
- <input ref={inputRef!} type="text" />
- <input ref={(el) => console.log('Element:', el)} type="email" />
- </form>
- );
- };
+ onMount(() => inputRef.focus());
- export { Form };
+ <input ref={inputRef!} />
+ <input ref={(el) => observe(el)} />
```
- **Why good:** No forwardRef wrapper needed, refs are just props, works with components and DOM elements
+ Full code: [examples/components.md](examples/components.md)
---
- ### Pattern 7: Context
+ ### Pattern 7: Context over a store
- Share data across component tree without prop drilling. Create a typed context, wrap in Provider with a Store, and expose a hook with error handling.
+ A context value built from a store needs getters on its fields; a plain property read would copy the value out of the graph at creation.
```typescript
const AuthContext = createContext<AuthContextValue>();
const AuthProvider: ParentComponent = (props) => {
const [store, setStore] = createStore<{ user: User | null }>({ user: null });
- const value = { get user() { return store.user; }, /* actions */ };
+ const signIn = (user: User) => setStore("user", user);
+ const value = { get user() { return store.user; }, signIn };
return <AuthContext.Provider value={value}>{props.children}</AuthContext.Provider>;
};
function useAuth(): AuthContextValue {
const ctx = useContext(AuthContext);
- if (!ctx) throw new Error('useAuth must be used within AuthProvider');
+ if (!ctx) throw new Error("useAuth must be used within AuthProvider");
return ctx;
}
```
- **Why good:** Getter on store field preserves reactivity in context, throws on missing provider
-
- See [examples/stores.md](examples/stores.md) for complete Store + Context implementation.
+ Full code: [examples/stores.md](examples/stores.md)
</patterns>
---
- <integration>
-
- ## Integration Guide
-
- **SolidJS is framework-agnostic for styling and tooling.** Components receive props and emit events, fitting any styling or state management approach.
-
- **Ecosystem:**
-
- - **SolidStart** for full-stack applications with file-based routing
- - **@solidjs/router** for client-side routing
- - **solid-primitives** community library for common utilities
- - Any CSS solution via `class` attribute binding
-
- **State decisions:**
-
- - Simple values: `createSignal`
- - Nested objects/arrays: `createStore`
- - Shared across components: Context + Store
- - Async data: `createResource` or `createAsync` (SolidStart)
-
- **Component communication:**
-
- - Props down, callbacks up (like React)
- - Context for deeply nested sharing
- - No prop drilling thanks to fine-grained reactivity
-
- </integration>
-
- ---
-
<red_flags>
- ## RED FLAGS
+ ## Red flags
- - **Reading signal without parentheses** - `count` instead of `count()` doesn't read the value or track dependencies
- - **Destructuring props** - `const { name } = props` breaks reactivity; use `props.name` or `splitProps()`
- - **Using ternary instead of Show** - `{condition ? <A /> : <B />}` bypasses Solid's optimizations
- - **Using .map() instead of For** - `{items().map(...)}` doesn't get fine-grained list updates
- - **Missing onCleanup in effects** - Event listeners, timers, subscriptions will leak memory
- - **Async operations inside createEffect tracking scope** - Code after `await` loses tracking context
- - **Side effects in createMemo** - Memos should be pure; use createEffect for side effects
- - **Using createEffect for data fetching** - Use createResource or createAsync instead
- - **Direct mutation of store** - `store.field = x` bypasses proxy tracking; use setStore path syntax
+ **Breaks at runtime:**
- **Gotchas:**
+ - A signal read without its parentheses — `{count}` renders the function's source and tracks nothing
+ - Destructured props — the value is captured once and never updates again
+ - A store mutated in place, `store.field = x` — the assignment goes around the proxy, so nothing is notified; use `setStore` path syntax or `produce`
+ - An effect that opens a listener, timer or subscription without `onCleanup` — it leaks one per re-run, not just one per component
+ - Signals read after an `await` inside an effect — the tracking scope ended at the `await`, so those reads register no dependency; read them first
+ - `createEffect` used to fetch — no cancellation, no `<Suspense>`, and a race whenever the input changes faster than the response returns
- - Signals read outside reactive context (event handlers) aren't tracked
- - Stores only track property access (`store.field`), not the store object itself
- - Code after `await` in effects runs outside the tracking scope
- - `children` is a getter in Solid - use `children()` helper when iterating
- - Index provides values as signals - call `item()` inside Index, not in For
+ **Surprising behaviour:**
- See [reference.md](reference.md) for full anti-pattern examples and decision frameworks.
+ - A component body runs once, so a `console.log` there fires at creation and never again
+ - `<Index>` hands each item as a signal (`item()`) while `<For>` hands the value directly — swapping one for the other compiles and renders nothing useful
+ - `props.children` is a getter; iterating it more than once needs the `children()` helper
+ - Signal reads inside an event handler are untracked, because a handler is outside any reactive scope
+ - A store tracks property access, not the store object, so passing `store` somewhere and reading it there subscribes to nothing
+ - A side effect inside `createMemo` runs on an unpredictable schedule — a memo is meant to be pure
</red_flags>
-
- ---
-
- <critical_reminders>
-
- ## CRITICAL REMINDERS
-
- > **All code must follow project conventions in CLAUDE.md**
-
- **(You MUST call signals as functions to read values - `count()` NOT `count`)**
-
- **(You MUST NEVER destructure props - use `props.name` or `splitProps()` to preserve reactivity)**
-
- **(You MUST use `<Show>`, `<For>`, `<Switch>` control flow components instead of ternaries and `.map()`)**
-
- **(You MUST clean up side effects with `onCleanup()` inside effects)**
-
- **(You MUST wrap async data fetching in `createResource` and components in `<Suspense>`)**
-
- **Failure to follow these rules will break reactivity, cause memory leaks, or result in stale UI.**
-
- </critical_reminders>