solid-js-best-practices

Installation
SKILL.md

Solid.js Best Practices

Comprehensive best practices for building Solid.js applications and components, optimized for AI-assisted code generation, review, and refactoring.

Quick Reference

Essential Imports

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";

Component Skeleton

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;

Rules by Category

1. Reactivity (7 rules)

# 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

2. Components (10 rules)

# 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

3. Control Flow (6 rules)

# 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

4. State Management (5 rules)

# 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

5. Refs & DOM (7 rules)

# 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

6. Performance (6 rules)

# 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

7. Accessibility (3 rules)

# 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

8. Testing (11 rules)

# 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

Task-Based Rule Selection

Writing New Components

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

Code Review

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

Performance Optimization

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

State Management

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

Accessibility Audit

Load these rules when auditing accessibility:

Rule Focus
7-1 Semantic structure
7-2 Screen reader support
7-3 Keyboard users

Writing Tests

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

Integrating Web Components / Custom Elements

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

Common Mistakes to Catch

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-changeon: 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

Solid.js vs React Mental Model

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

Priority Levels

  • CRITICAL: Fix immediately. Causes bugs, broken reactivity, or runtime errors.
  • HIGH: Address in code reviews. Important for correctness and maintainability.
  • MEDIUM: Apply when relevant. Improves code quality and performance.
  • LOW: Consider during refactoring. Nice-to-have optimizations.

Key Solid.js Concepts

Fine-Grained Reactivity

Solid.js updates only the specific DOM elements that depend on changed data, not entire component trees. This is achieved through:

  • Signals: Reactive primitives that track dependencies
  • Effects: Side effects that automatically re-run when dependencies change
  • Memos: Cached derived values that only recompute when dependencies change

Components Render Once

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:

  • Props must not be destructured (would capture static values)
  • Signals must be called as functions (to maintain reactive tracking)
  • Control flow uses special components (<Show>, <For>) instead of JS expressions

Stores for Complex State

For nested objects and arrays, Solid provides stores with:

  • Fine-grained updates via path syntax
  • Automatic proxy wrapping for nested reactivity
  • Utilities like produce and reconcile for common patterns

Tooling

For 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.

Resources

Installs
37
GitHub Stars
1
First Seen
Jan 25, 2026