.agents/skills/solid-js-best-practices/SKILL.md
Solid.js best practices for AI-assisted code generation, code review, refactoring, and debugging reactivity issues. Use when writing Solid.js components, auditing SolidJS code, migrating from React to Solid, fixing signals and fine-grained reactivity bugs, or integrating web component libraries. 55 rules across 8 categories (reactivity, components, control flow, state management, refs/DOM, performance, accessibility, testing) ranked by priority.
npx skillsauth add em-jones/staccato-toolkit solid-js-best-practicesInstall 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.
Comprehensive best practices for building Solid.js applications and components, optimized for AI-assisted code generation, review, and refactoring.
import {
createSignal,
createEffect,
createMemo,
createResource,
onMount,
onCleanup,
Show,
For,
Switch,
Match,
Index,
Suspense,
ErrorBoundary,
lazy,
batch,
untrack,
mergeProps,
splitProps,
children,
} from "solid-js";
import { createStore, produce, reconcile } from "solid-js/store";
import { Component, JSX, mergeProps, splitProps } from "solid-js";
interface MyComponentProps {
title: string;
count?: number;
onAction?: () => void;
children?: JSX.Element;
}
const MyComponent: Component<MyComponentProps> = (props) => {
// Merge default props
const merged = mergeProps({ count: 0 }, props);
// Split component props from passed-through props
const [local, others] = splitProps(merged, ["title", "count", "onAction"]);
// Local reactive state
const [value, setValue] = createSignal("");
// Derived/computed values
const doubled = createMemo(() => local.count * 2);
// Side effects
createEffect(() => {
console.log("Count changed:", local.count);
});
// Lifecycle
onMount(() => {
console.log("Component mounted");
});
onCleanup(() => {
console.log("Component cleanup");
});
return (
<div {...others}>
<h1>{local.title}</h1>
<p>
Count: {local.count}, Doubled: {doubled()}
</p>
<input value={value()} onInput={(e) => setValue(e.currentTarget.value)} />
<button onClick={local.onAction}>Action</button>
{props.children}
</div>
);
};
export default MyComponent;
| # | Rule | Priority | Description |
| ------------------------------------------------------ | ---------------------------------- | -------- | ---------------------------------------------------------------------- |
| 1-1 | Use Signals Correctly | CRITICAL | Always call signals as functions count() not count |
| 1-2 | Use Memo for Derived Values | HIGH | Use createMemo for computed values, not createEffect |
| 1-3 | Effects for Side Effects Only | HIGH | Use createEffect only for side effects, not derivations |
| 1-7 | No Primitives in Reactive Contexts | HIGH | Don't call hooks or create reactive primitives inside effects or memos |
| 1-4 | Avoid Setting Signals in Effects | MEDIUM | Setting signals in effects can cause infinite loops |
| 1-5 | Use Untrack When Needed | MEDIUM | Use untrack() to prevent unwanted reactive subscriptions |
| 1-6 | Batch Signal Updates | LOW | Use batch() for multiple synchronous signal updates |
| # | Rule | Priority | Description |
| ------------------------------------------------------------ | -------------------------------------- | -------- | ------------------------------------------------------------------------------------ |
| 2-1 | Never Destructure Props | CRITICAL | Destructuring props breaks reactivity |
| 2-6 | Components Return Once | CRITICAL | Never use early returns — use <Show>, <Switch>, etc. in JSX |
| 2-9 | Never Call Components as Functions | CRITICAL | Always use JSX or createComponent() — direct calls leak reactive scope |
| 2-2 | Use mergeProps | HIGH | Use mergeProps for default prop values |
| 2-3 | Use splitProps | HIGH | Use splitProps to separate prop groups safely |
| 2-7 | No React-Specific Props | HIGH | Use class not className, for not htmlFor |
| 2-10 | Custom Element TypeScript Declarations | HIGH | Declare custom element tags in JSX namespace; augment DOM types for newer attributes |
| 2-4 | Use children Helper | MEDIUM | Use children() helper for safe children access |
| 2-5 | Prefer Composition | MEDIUM | Prefer composition and context over prop drilling |
| 2-8 | Style Prop Conventions | MEDIUM | Use object syntax with kebab-case properties for style |
| # | Rule | Priority | Description |
| --------------------------------------------- | ------------------------- | -------- | ------------------------------------------------------------------- |
| 3-1 | Use Show for Conditionals | HIGH | Use <Show> instead of ternary operators |
| 3-2 | Use For for Lists | HIGH | Use <For> for referentially-keyed list rendering |
| 3-3 | Use Index for Primitives | MEDIUM | Use <Index> when array index matters more than identity |
| 3-4 | Use Switch/Match | MEDIUM | Use <Switch>/<Match> for multiple conditions |
| 3-6 | Stable Component Mount | MEDIUM | Avoid rendering the same component in multiple Switch/Show branches |
| 3-5 | Provide Fallbacks | LOW | Always provide fallback props for loading states |
| # | Rule | Priority | Description |
| --------------------------------------------- | ----------------------------- | -------- | ----------------------------------------------------- |
| 4-1 | Signals vs Stores | HIGH | Use signals for primitives, stores for nested objects |
| 4-2 | Use Store Path Syntax | HIGH | Use path syntax for granular, efficient store updates |
| 4-3 | Use produce for Mutations | MEDIUM | Use produce for complex mutable-style store updates |
| 4-4 | Use reconcile for Server Data | MEDIUM | Use reconcile when integrating server/external data |
| 4-5 | Use Context for Global State | MEDIUM | Use Context API for cross-component shared state |
| # | Rule | Priority | Description |
| -------------------------------------------------- | ------------------------------ | -------- | --------------------------------------------------------------------------------- |
| 5-1 | Use Refs Correctly | HIGH | Use callback refs for conditional elements |
| 5-2 | Access DOM in onMount | HIGH | Access DOM elements in onMount, not during render |
| 5-3 | Cleanup with onCleanup | HIGH | Always clean up subscriptions and timers |
| 5-5 | Avoid innerHTML | HIGH | Avoid innerHTML to prevent XSS — use JSX or textContent |
| 5-7 | Web Component Controlled State | HIGH | Use createEffect + ref + imperative calls to sync signals to web component APIs |
| 5-4 | Use Directives | MEDIUM | Use use: directives for reusable element behaviors |
| 5-6 | Event Handler Patterns | MEDIUM | Use on:/oncapture: namespaces and array handler syntax correctly |
| # | Rule | Priority | Description |
| ------------------------------------------------ | ------------------------------------- | -------- | --------------------------------------------------------------------------------- |
| 6-1 | Avoid Unnecessary Tracking | HIGH | Don't access signals outside reactive contexts |
| 6-2 | Use Lazy Components | MEDIUM | Use lazy() for code splitting large components |
| 6-3 | Use Suspense | MEDIUM | Use <Suspense> for async loading boundaries |
| 6-6 | Web Component CSS and Bundle Strategy | MEDIUM | Import components individually; place ::part() overrides in a global stylesheet |
| 6-4 | Optimize Store Access | LOW | Access only the store properties you need |
| 6-5 | Prefer classList | LOW | Use classList prop for conditional class toggling |
| # | Rule | Priority | Description | | --------------------------------------- | --------------------------- | -------- | ------------------------------------------------------- | | 7-1 | Use Semantic HTML | HIGH | Use appropriate semantic HTML elements | | 7-2 | Use ARIA Attributes | MEDIUM | Apply appropriate ARIA attributes for custom controls | | 7-3 | Support Keyboard Navigation | MEDIUM | Ensure all interactive elements are keyboard accessible |
| # | Rule | Priority | Description |
| ---------------------------------------------------------------- | ---------------------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------- |
| 8-1 | Configure Vitest for Solid | CRITICAL | Configure Vitest with Solid-specific resolve conditions and plugin |
| 8-2 | Wrap Render in Arrow Functions | CRITICAL | Always use render(() => <C />) not render(<C />) |
| 8-3 | Test Primitives in a Root | HIGH | Wrap signal/effect/memo tests in createRoot or renderHook |
| 8-4 | Handle Async in Tests | HIGH | Use findBy queries and proper timer config for async behavior |
| 8-5 | Use Accessible Queries | MEDIUM | Prefer role and label queries over test IDs |
| 8-6 | Separate Logic from UI Tests | MEDIUM | Test primitives/hooks independently from component rendering |
| 8-7 | Browser Mode for Web Components and PWA APIs | HIGH | Use Vitest browser mode (real Chromium) for custom elements, shadow DOM, and browser-native APIs |
| 8-8 | Testing Headless UI Libraries with Non-Standard ARIA | MEDIUM | Headless UI libraries use non-obvious ARIA structures and portals — inspect the actual tree before querying |
| 8-9 | Browser-Native API Test Isolation | HIGH | Clear IndexedDB and localStorage between tests — close connection before deleteDatabase |
| 8-10 | Router Integration Testing | HIGH | Use MemoryRouter root prop to provide router context to layout providers |
| 8-11 | TanStack Query Test Setup | HIGH | Create a fresh QueryClient per test with retry and caching disabled |
Load these rules when creating new Solid.js components:
| Rule | Why | | ------------------------------------------------------ | ------------------------------------------ | | 1-1 | Ensure signals are called as functions | | 2-1 | Prevent reactivity breakage | | 2-6 | No early returns — use control flow in JSX | | 2-9 | Never call components as plain functions | | 2-2 | Handle default props correctly | | 2-3 | Separate local and forwarded props | | 3-1 | Proper conditional rendering | | 3-2 | Efficient list rendering | | 5-3 | Prevent memory leaks |
Focus on these rules during code review:
| Priority | Rules | | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | CRITICAL | 1-1, 2-1, 2-6, 2-9 | | HIGH | 1-2, 1-3, 1-7, 2-7, 5-2, 5-3, 5-5 |
Load these rules when optimizing performance:
| Rule | Focus | | ---------------------------------------------- | --------------------------------- | | 1-2 | Prevent unnecessary recomputation | | 1-6 | Reduce update cycles | | 4-2 | Granular store updates | | 6-1 | Prevent unwanted subscriptions | | 6-2 | Code splitting | | 6-4 | Efficient store access |
Load these rules when working with application state:
| Rule | Focus | | --------------------------------------------- | -------------------------- | | 4-1 | Choose the right primitive | | 4-2 | Efficient updates | | 4-3 | Complex mutations | | 4-4 | External data integration | | 4-5 | Cross-component state |
Load these rules when auditing accessibility:
| Rule | Focus | | --------------------------------------- | --------------------- | | 7-1 | Semantic structure | | 7-2 | Screen reader support | | 7-3 | Keyboard users |
Load these rules when writing or reviewing tests:
| Rule | Focus | | ---------------------------------------------------------------- | ---------------------------------------- | | 8-1 | Correct Vitest configuration | | 8-2 | Reactive render scope | | 8-3 | Reactive ownership for primitives | | 8-4 | Async queries and timers | | 8-5 | Accessible query selection | | 8-6 | Test architecture | | 8-7 | When to use browser mode vs jsdom | | 8-8 | Portals and non-standard ARIA structures | | 8-9 | IDB and localStorage cleanup patterns | | 8-10 | MemoryRouter setup for integration tests | | 8-11 | QueryClient configuration for tests |
Load these rules when using any custom element library (Shoelace, FAST, Lion, Material Web Components, etc.) or native browser APIs like <dialog> and the Popover API:
| Rule | Why |
| ------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------- |
| 2-10 | Declare custom element tags in JSX namespace; type newer HTML attributes and experimental CSS properties |
| 5-6 | Use on: for all custom element events; type CustomEvent payloads correctly |
| 5-7 | Sync Solid signals to web component / native browser API imperative calls |
| 6-6 | Per-component imports for tree-shaking; ::part() overrides in global CSS only |
| Mistake | Rule | Solution |
| ------------------------------------------------------------------------ | ---------------------------------------------------------------- | ------------------------------------------------------------------------------ |
| Forgetting () on signal access | 1-1 | Always call signals: count() |
| Destructuring props | 2-1 | Access via props.name |
| Using ternaries for conditionals | 3-1 | Use <Show> component |
| .map() for lists | 3-2 | Use <For> component |
| Deriving values in effects | 1-2 | Use createMemo |
| Setting signals in effects | 1-4 | Use createMemo or external triggers |
| Accessing DOM during render | 5-2 | Use onMount |
| Forgetting cleanup | 5-3 | Use onCleanup |
| Early returns in components | 2-6 | Use <Show>, <Switch> in JSX instead |
| Using className or htmlFor | 2-7 | Use class and for (standard HTML) |
| style="color: red" or camelCase styles | 2-8 | Use style={{ color: "red" }} with kebab-case |
| Using innerHTML with user data | 5-5 | Use JSX or sanitize with DOMPurify |
| Spreading whole store | 6-4 | Access specific properties |
| String concatenation for class toggling | 6-5 | Use classList={{ active: isActive() }} |
| render(<Comp />) without arrow | 8-2 | Use render(() => <Comp />) |
| Effects in tests without owner | 8-3 | Wrap in createRoot or use renderHook |
| getBy for async content | 8-4 | Use findBy queries |
| MyComp(props) instead of <MyComp /> | 2-9 | Always use JSX syntax or createComponent() |
| Calling useMatch()/useQuery() inside createEffect/createComputed | 1-7 | Call hooks once at component init, not inside reactive computations |
| Same component in Switch fallback and Match branch | 3-6 | Keep component in one stable position; use CSS for layout changes |
| Custom elements don't upgrade / lifecycle doesn't fire in tests | 8-7 | Use Vitest browser mode (real Chromium) instead of jsdom |
| IDB state persists between tests causing order-dependent failures | 8-9 | Close connection before deleteDatabase; use useCleanDb() |
| Router primitives throw "can only be used inside a Route" | 8-10 | Use MemoryRouter root prop with a layout factory |
| QueryClient retries mask errors / cache leaks between tests | 8-11 | Use makeTestQueryClient() with retry: false, gcTime: 0 |
| waitFor(length === 0) passes before data loads | 8-4 | Use a settled anchor with findBy before asserting absence |
| getByRole('form') throws even though the form exists | 7-2 | Add aria-label or aria-labelledby to expose role="form" |
| <my-element onMyChange={...}> misses all events | 5-6 | Use on:my-change — on: prefix required for all web component custom events |
| my-element::part(...) rule inside a .module.css is silently ignored | 6-6 | Move ::part() overrides to a non-module global stylesheet |
| Barrel import of entire web component library | 6-6 | Import individual components by path to enable tree-shaking |
| value={signal()} on web component — no two-way sync | 5-7 | Listen to change events; push value imperatively via ref + createEffect |
| <div popover> or <button popoverTarget="x"> TypeScript error | 2-10 | Augment HTMLElement / HTMLButtonElement in a .d.ts file |
| Object/array prop on custom element becomes "[object Object]" | 5-7 | Use prop:myProp={value()} to set a JS property, not an HTML attribute |
| Experimental CSS property (anchor-name) produces a TypeScript error | 2-8 | Cast with as unknown as JSX.CSSProperties instead of as never |
When helping users familiar with React, keep these differences in mind:
| React | Solid.js |
| -------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| Components re-render on state change | Components run once, signals update DOM directly |
| useState returns [value, setter] | createSignal returns [getter, setter] |
| useMemo with deps array | createMemo with automatic tracking |
| useEffect(fn, [deps]) | createEffect(fn) (no deps array — automatic tracking) |
| Destructure props freely | Never destructure props |
| Early returns (if (!x) return null) | <Show> / <Switch> in JSX (components return once) |
| {condition && <Component />} | <Show when={condition}><Component /></Show> |
| {items.map(item => ...)} | <For each={items}>{item => ...}</For> |
| className | class |
| htmlFor | for |
| style={{ fontSize: 14 }} | style={{ "font-size": "14px" }} |
| Context requires useContext hook | Context works with useContext or direct access |
| React 18: ref + addEventListener for custom element events; React 19: onMyEvent={handler} natively | on:my-event={handler} — always use on: prefix with web component events |
Solid.js updates only the specific DOM elements that depend on changed data, not entire component trees. This is achieved through:
Unlike React, Solid components are functions that run once during initial render. Reactivity happens at the signal level, not the component level. This is why:
<Show>, <For>) instead of JS expressionsFor nested objects and arrays, Solid provides stores with:
produce and reconcile for common patternsFor automated linting alongside these best practices, use eslint-plugin-solid. The plugin catches many of the same issues this skill covers (destructured props, early returns, React-specific props, innerHTML usage, style prop format, etc.) and provides auto-fixable rules.
tools
<!--VITE PLUS START--> # Using Vite+, the Unified Toolchain for the Web This project is using Vite+, a unified toolchain built on top of Vite, Rolldown, Vitest, tsdown, Oxlint, Oxfmt, and Vite Task. Vite+ wraps runtime management, package management, and frontend tooling in a single global CLI called `vp`. Vite+ is distinct from Vite, but it invokes Vite through `vp dev` and `vp build`. ## Vite+ Workflow `vp` is a global binary that handles the full development lifecycle. Run `vp help` to pr
development
Guide for building performant data tables. Uses tanstack-table for table logic (sorting, filtering, pagination) and tanstack-virtual for rendering large datasets efficiently.
development
Expert guidance for building observable, expressive, and fault-tolerant TypeScript applications using the effect-ts/effect ecosystem. Covers Effect<A, E, R> type, error management, dependency injection via Layers, observability (logging, metrics, tracing), concurrency with Fibers, retry/scheduling, Schema validation, Streams, and Sinks.
tools
Complete E2E (end-to-end) and integration testing skill for TypeScript/NestJS projects using Jest, real infrastructure via Docker, and GWT pattern. ALWAYS use this skill when user needs to: **SETUP** - Initialize or configure E2E testing infrastructure: - Set up E2E testing for a new project - Configure docker-compose for testing (Kafka, PostgreSQL, MongoDB, Redis) - Create jest-e2e.config.ts or E2E Jest configuration - Set up test helpers for database, Kafka, or Redis - Configure .env.e2e environment variables - Create test/e2e directory structure **WRITE** - Create or add E2E/integration tests: - Write, create, add, or generate e2e tests or integration tests - Test API endpoints, workflows, or complete features end-to-end - Test with real databases, message brokers, or external services - Test Kafka consumers/producers, event-driven workflows - Working on any file ending in .e2e-spec.ts or in test/e2e/ directory - Use GWT (Given-When-Then) pattern for tests **REVIEW** - Audit or evaluate E2E tests: - Review existing E2E tests for quality - Check test isolation and cleanup patterns - Audit GWT pattern compliance - Evaluate assertion quality and specificity - Check for anti-patterns (multiple WHEN actions, conditional assertions) **RUN** - Execute or analyze E2E test results: - Run E2E tests - Start/stop Docker infrastructure for testing - Analyze E2E test results - Verify Docker services are healthy - Interpret test output and failures **DEBUG** - Fix failing or flaky E2E tests: - Fix failing E2E tests - Debug flaky tests or test isolation issues - Troubleshoot connection errors (database, Kafka, Redis) - Fix timeout issues or async operation failures - Diagnose race conditions or state leakage - Debug Kafka message consumption issues **OPTIMIZE** - Improve E2E test performance: - Speed up slow E2E tests - Optimize Docker infrastructure startup - Replace fixed waits with smart polling - Reduce beforeEach cleanup time - Improve test parallelization where safe Keywords: e2e, end-to-end, integration test, e2e-spec.ts, test/e2e, Jest, supertest, NestJS, Kafka, Redpanda, PostgreSQL, MongoDB, Redis, docker-compose, GWT pattern, Given-When-Then, real infrastructure, test isolation, flaky test, MSW, nock, waitForMessages, fix e2e, debug e2e, run e2e, review e2e, optimize e2e, setup e2e