dist/plugins/web-framework-solidjs/skills/web-framework-solidjs/SKILL.md
SolidJS fine-grained reactivity patterns - signals, effects, memos, stores, createResource, control flow components, Suspense, SolidStart
npx skillsauth add agents-inc/skills web-framework-solidjsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Quick Guide: Use
createSignalfor primitives,createStorefor nested objects. Always call signals as functions (count()notcount). Never destructure props. Use<Show>,<For>,<Switch>for control flow. Wrap async data increateResourceand components in<Suspense>.
<critical_requirements>
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(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>)
</critical_requirements>
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
When to use:
Key patterns covered:
When NOT to use:
Detailed Resources:
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:
count()) subscribes to it, creating automatic dependency trackingcreateSignal and createStoreKey mental model:
// 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
}
// 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
}
</philosophy>
Signals are the foundation of Solid's reactivity. They hold a value and notify subscribers when it changes.
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 [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
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 };
Why good: Component body runs once, only {count()} expression re-evaluates on update, minimal DOM manipulation
Effects run automatically when their tracked dependencies change.
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());
window.addEventListener("click", handler);
// MUST clean up to prevent memory leaks
onCleanup(() => {
window.removeEventListener("click", handler);
});
});
Why good: Automatic dependency tracking (no dependency array), onCleanup runs before each re-execution and on disposal
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);
}),
);
Why good: Explicit control over what triggers the effect, access to previous value
Memos cache computed values and only recalculate when dependencies change.
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));
});
Why good: Caches result until dependencies change, prevents unnecessary recalculations, clearer than inline expressions for complex logic
Never destructure props in Solid - it breaks reactivity.
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']);
return (
<button
{...buttonProps}
class={`btn btn-${local.variant}`}
disabled={local.loading || buttonProps.disabled}
>
{local.loading ? 'Loading...' : props.children}
</button>
);
};
export { Button };
Why good: splitProps separates custom props from spread-able HTML props, mergeProps provides defaults while preserving reactivity, never destructure props
Use VoidComponent (no children), ParentComponent (children required), or Component (children optional) for type-safe children handling.
const Icon: VoidComponent<{ name: string }> = (props) => (/* ... */);
const Card: ParentComponent<{ title: string }> = (props) => (/* ... */);
See examples/components.md for full component type examples.
Solid uses dedicated components for control flow instead of JavaScript expressions.
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
import { For } from 'solid-js';
// Basic list rendering
<For each={items()} fallback={<p>No items</p>}>
{(item, index) => (
<li>
{index()}: {item.name}
</li>
)}
</For>
Why good: Automatically handles keying by reference, index() is a signal, optimized list diffing
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>
Why good: First matching condition renders, cleaner than nested Shows
Refs work differently in Solid - no forwardRef needed.
import { onMount, type Component } from 'solid-js';
const Form: Component = () => {
let inputRef: HTMLInputElement;
onMount(() => {
// Ref is available after mount
inputRef.focus();
});
return (
<form>
{/* Ref callback or assignment */}
<input ref={inputRef!} type="text" />
<input ref={(el) => console.log('Element:', el)} type="email" />
</form>
);
};
export { Form };
Why good: No forwardRef wrapper needed, refs are just props, works with components and DOM elements
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.
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 */ };
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');
return ctx;
}
Why good: Getter on store field preserves reactivity in context, throws on missing provider
See examples/stores.md for complete Store + Context implementation.
</patterns>SolidJS is framework-agnostic for styling and tooling. Components receive props and emit events, fitting any styling or state management approach.
Ecosystem:
class attribute bindingState decisions:
createSignalcreateStorecreateResource or createAsync (SolidStart)Component communication:
<red_flags>
count instead of count() doesn't read the value or track dependenciesconst { name } = props breaks reactivity; use props.name or splitProps(){condition ? <A /> : <B />} bypasses Solid's optimizations{items().map(...)} doesn't get fine-grained list updatesawait loses tracking contextstore.field = x bypasses proxy tracking; use setStore path syntaxGotchas:
store.field), not the store object itselfawait in effects runs outside the tracking scopechildren is a getter in Solid - use children() helper when iteratingitem() inside Index, not in ForSee reference.md for full anti-pattern examples and decision frameworks.
</red_flags>
<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>
development
Material Design component library for Vue 3
development
VitePress 1.x — Vue-powered static site generator for documentation sites, built on Vite
tools
Docusaurus 3.x documentation framework — site configuration, docs/blog plugins, sidebars, versioning, MDX, swizzling, and deployment
development
TanStack Form patterns - useForm, form.Field, validators, arrays, linked fields, createFormHook, type safety