Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Best practices and idiomatic patterns for SolidJS development. Use when writing, reviewing, or refactoring SolidJS components, signals, stores, effects, or reactive primitives. Triggers on: SolidJS, Solid.js, solid-js, createSignal, createEffect, createMemo, createStore, createResource, or when the project uses SolidJS as its UI framework.
.claude/skills/nexxeln-solidjs/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-06 | ✗→✓ | ▲ Improved | 58% | 0% |
| case-11 | ✗→✓ | ▲ Improved | 11% | 0% |
| case-17 | ✓→✓ | = Same ✓ | 494% | 0% |
| case-01 | ✓→✓ | = Same ✓ | 129% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 146% | 0% |
SolidJS components run once as setup code. The component body is not reactive. Reactivity only occurs inside reactive scopes: JSX expressions, createEffect, createMemo, and createResource.
All guidance below follows from this core principle.
Pass id={id()} not id={id}. Components should not need to know whether a prop came from a signal or a static value. Calling the accessor at the call site keeps prop types uniform.
tsx// correct <User id={id()} name="Brenley" /> // wrong — forces User to accept Accessor<number> for id <User id={id} name="Brenley" />
Props are reactive via getters. Destructuring extracts the value and breaks tracking.
tsx// broken — name is no longer reactive function User(props: { name: string }) { const { name } = props; return <h1>{name}</h1>; } // correct function User(props: { name: string }) { return <h1>{props.name}</h1>; }
Use splitProps when prop splitting is genuinely needed.
A signal read in the component body runs once and never updates.
tsx// broken — doubled is a static number const doubled = count() * 2; // reactive — function wrapper defers the read to a reactive scope const doubled = () => count() * 2; // reactive + cached — createMemo avoids redundant recalculation const doubled = createMemo(() => count() * 2);
Use a plain function wrapper when the computation is trivial. Use createMemo when the derived value is read in multiple places or the computation is expensive.
<Show> and <For> instead of JS conditionals/mapSolid provides control-flow components that integrate with its reactive system.
tsx// prefer <Show when={open()} fallback={<EmptyState />}> <SidebarMenu /> </Show> // instead of {open() && <SidebarMenu />}
tsx// prefer <For each={items()}>{item => <Item item={item} />}</For> // instead of {items().map(item => <Item item={item} />)}
<For> preserves item identity and applies minimal DOM updates. <Show> gives explicit conditional rendering with fallback support.
createEffect is for side effects that interact with the outside world (DOM APIs, third-party libraries). It is not for derived state or data fetching.
Anti-pattern — derived state via effect:
tsxconst [count, setCount] = createSignal(0); const [double, setDouble] = createSignal(0); createEffect(() => setDouble(count() * 2)); // don't // instead, derive it const double = createMemo(() => count() * 2);
Anti-pattern — data fetching via effect:
tsxconst [posts, setPosts] = createSignal([]); createEffect(async () => { const data = await fetch("/api/posts").then(r => r.json()); setPosts(data); // don't — race conditions, no loading/error state }); // instead, use createResource const [posts] = createResource(() => fetch("/api/posts").then(r => r.json()));
createResource integrates with Suspense and handles loading, error states, and race conditions.
When one value is a pure function of another, express that relationship directly instead of syncing via effects.
tsx// anti-pattern — manual sync const [firstName, setFirstName] = createSignal("John"); const [lastName, setLastName] = createSignal("Doe"); const [fullName, setFullName] = createSignal(""); createEffect(() => setFullName(`${firstName()} ${lastName()}`)); // correct — derived value const fullName = () => `${firstName()} ${lastName()}`;
Derived values keep the dependency graph explicit. Effects hide relationships and add unnecessary indirection.
Signals replace the entire value on update. Stores provide fine-grained reactivity at the property level.
tsx// prefer stores for objects const [board, setBoard] = createStore({ boards: ["Board 1", "Board 2"], notes: ["Note 1", "Note 2"], }); // granular update — only notes subscribers react setBoard("notes", notes => [...notes, "Note 3"]);
tsx// avoid signals for objects — replaces entire value, all readers react const [board, setBoard] = createSignal({ boards: ["Board 1", "Board 2"], notes: ["Note 1", "Note 2"], }); setBoard({ ...board(), notes: [...board().notes, "Note 3"] });
Stores are deeply reactive. Accessing board.settings.theme in JSX tracks only that path.
When writing or reviewing SolidJS code, verify:
signal() not signal)props.x, never destructuredcreateMemo, not bare reads in component body<Show> and <For> are used instead of inline JS conditionals and .map()createEffect is only used for genuine side effects (DOM, external APIs)createStore instead of createSignal| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-17 | pass→pass | 1,938 | 1,682 | -13% | 1 | 1 | 0% | 261 | 1,550 | +494% | 0 | 0 | — |
case-01 | pass→pass | 4,226 | 1,514 | -64% | 1 | 1 | 0% | 672 | 1,542 | +129% | 0 | 0 | — |
case-02 | pass→pass | 4,407 | 3,458 | -22% | 1 | 1 | 0% | 773 | 1,899 | +146% | 0 | 0 | — |
case-03 | pass→pass | 10,048 | 7,351 | -27% | 1 | 1 | 0% | 1,994 | 2,772 | +39% | 0 | 0 | — |
case-04 | pass→pass | 5,673 | 2,247 | -60% | 1 | 1 | 0% | 944 | 1,680 | +78% | 0 | 0 | — |
case-05 | pass→pass | 8,689 | 7,545 | -13% | 1 | 1 | 0% | 1,719 | 2,758 | +60% | 0 | 0 | — |
case-06 | fail→pass | 7,263 | 3,567 | -51% | 1 | 1 | 0% | 1,222 | 1,926 | +58% | 0 | 0 | — |
case-07 | pass→pass | 6,706 | 3,379 | -50% | 1 | 1 | 0% | 1,188 | 1,838 | +55% | 0 | 0 | — |
case-08 | pass→pass | 8,727 | 6,972 | -20% | 1 | 1 | 0% | 1,739 | 2,624 | +51% | 0 | 0 | — |
case-09 | pass→pass | 7,279 | 6,949 | -5% | 1 | 1 | 0% | 1,392 | 2,627 | +89% | 0 | 0 | — |
case-10 | pass→pass | 6,935 | 3,589 | -48% | 1 | 1 | 0% | 1,206 | 1,920 | +59% | 0 | 0 | — |
case-11 | fail→pass | 17,523 | 12,591 | -28% | 1 | 1 | 0% | 3,483 | 3,882 | +11% | 0 | 0 | — |
case-12 | pass→pass | 7,760 | 2,913 | -62% | 1 | 1 | 0% | 1,462 | 1,839 | +26% | 0 | 0 | — |
case-13 | pass→pass | 6,876 | 3,549 | -48% | 1 | 1 | 0% | 1,193 | 1,909 | +60% | 0 | 0 | — |
case-14 | pass→pass | 11,175 | 6,316 | -43% | 1 | 1 | 0% | 1,867 | 2,364 | +27% | 0 | 0 | — |
case-15 | pass→pass | 9,905 | 6,130 | -38% | 1 | 1 | 0% | 1,747 | 2,385 | +37% | 0 | 0 | — |
case-16 | pass→pass | 4,151 | 4,309 | +4% | 1 | 1 | 0% | 685 | 2,053 | +200% | 0 | 0 | — |
case-18 | pass→pass | 1,896 | 2,276 | +20% | 1 | 1 | 0% | 288 | 1,550 | +438% | 0 | 0 | — |
case-19 | pass→pass | 9,087 | 4,931 | -46% | 1 | 1 | 0% | 1,542 | 2,090 | +36% | 0 | 0 | — |
case-20 | pass→pass | 3,569 | 2,018 | -43% | 1 | 1 | 0% | 381 | 1,638 | +330% | 0 | 0 | — |
case-21 | pass→pass | 6,007 | 4,345 | -28% | 1 | 1 | 0% | 1,090 | 2,079 | +91% | 0 | 0 | — |
case-22 | pass→pass | 9,435 | 7,269 | -23% | 1 | 1 | 0% | 1,898 | 2,853 | +50% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +9 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.