Die Oberfläche ist lokalisiert; Skill-Beschreibungen und -Dokumente werden per KI automatisch in Ihre Sprache übersetzt. Code, Befehle, Pfade und Fachbegriffe bleiben im Original, maßgeblich ist das Dokument des Autors.
Skills entdecken
Design und Kreativität · SKILL.md

frontend-solidjs-architecture

frontend-solidjs-architecture

Use this skill when the user says 'SolidJS', 'SolidJS architecture', 'Solid signal', 'Solid component', 'Solid reactivity', 'SolidJS app', 'Solid router', 'SolidJS vs React', 'SolidJS fine-grained'. This skill enforces: fine-grained reactivity via Signals/Memos/Effects, compiled JSX with no virtual DOM, declarative control flow components (Show/For/Switch),…

Independent GitHub snapshot · Automated classification · Original documentation governs

Skill-Dokumentation

GitHub

name: frontend-solidjs-architecture description: > Use this skill when the user says 'SolidJS', 'SolidJS architecture', 'Solid signal', 'Solid component', 'Solid reactivity', 'SolidJS app', 'Solid router', 'SolidJS vs React', 'SolidJS fine-grained'. This skill enforces: fine-grained reactivity via Signals/Memos/Effects, compiled JSX with no virtual DOM, declarative control flow components (Show/For/Switch), createStore for deep reactivity, and @solidjs/router for routing. Requires SolidJS project (package.json with solid-js). Do NOT use for: React, Vue, or Svelte projects. version: "1.0.0" author: "j4flmao" license: "MIT" compatibility: claude-code: true cursor: true codex: true windsurf: true tags: [frontend, solidjs, phase-7]

SolidJS Architecture

Purpose

Build applications with SolidJS fine-grained reactivity — no virtual DOM, compiled JSX, signals-based state management, and declarative control flow.

Agent Protocol

Trigger

Exact user phrases: "SolidJS", "SolidJS architecture", "Solid signal", "Solid component", "Solid reactivity", "SolidJS app", "Solid router", "SolidJS vs React", "SolidJS fine-grained".

Input Context

Before activating, verify:

  • package.json has solid-js dependency.
  • Whether the project uses Vite with the Solid plugin.
  • Whether @solidjs/router is installed for routing.

Output Artifact

No file output. Produces component patterns, signal/store usage, control flow examples, and routing setup as text.

Response Format

Code: show signal/component definitions. No imports. Control flow with Show/For/Switch.

No preamble. No postamble. No explanations. No filler/hedging/transitions. Compress output — why use many token when few do trick.

Completion Criteria

  • Components use signals (createSignal) for local state.
  • Derived values use createMemo, not createEffect.
  • Deep state uses createStore with path syntax for updates.
  • Control flow uses , , , components.
  • @solidjs/router configured with lazy loading for code splitting.
  • No use of hooks rules — components run once.
  • JSX compiles to real DOM expressions, not vdom.

Max Response Length

Code: 15 lines per example. Unlimited patterns.

Component Architecture / Decision Trees

Architecture Options

ApproachTrade-offWhen to Use
createSignal for local stateSimple, per-value granularityIndividual values, toggles, inputs
createStore for deep objectsPath-based updates, nested trackingForms, complex state objects
createMutable for mutable syntaxDirect mutation, less boilerplateLegacy code migration, prototyping
Context + createContextShared state via provider treeTheming, auth, i18n
Resources (createResource)Async data with SuspenseAPI data fetching
createMemo for derived stateCached computationComputed values from signals/stores

Decision Tree: State Management

Is the state local to one component?
  ├── Is it a single value? -> createSignal
  ├── Is it a complex object? -> createStore
  └── Is it an async resource? -> createResource
  └── No (shared) -> Is it tree-scoped?
       ├── Yes -> Context + createContext
       └── No -> Module-level signal or store

Decision Tree: Control Flow

Are you conditionally showing content?
  ├── Simple if/else -> <Show>
  ├── Multiple conditions -> <Switch> + <Match>
  └── List rendering:
       ├── Array of primitives -> <Index>
       └── Array of objects -> <For> (with stable keys)

Decision Tree: Effect Strategy

What kind of side effect?
  ├── React to signal change -> createEffect with tracking
  ├── DOM mutation after render -> createEffect (DOM access safe here)
  ├── Cleanup on dispose -> onCleanup inside createEffect
  └── No tracking needed -> createRenderEffect (runs before DOM paint)

Component Design Patterns

Counter with createSignal

function Counter(props: { initial?: number }) {
  const [count, setCount] = createSignal(props.initial ?? 0)
  const doubled = createMemo(() => count() * 2)
  return (
    <div>
      <p>Count: {count()}</p>
      <p>Doubled: {doubled()}</p>
      <button onClick={() => setCount(c => c + 1)}>+</button>
      <button onClick={() => setCount(c => c - 1)}>-</button>
    </div>
  )
}

Form with createStore

function SignupForm() {
  const [form, setForm] = createStore({
    email: '', password: '', confirmPassword: '',
    errors: {} as Record<string, string>,
  })
  const [submitted, setSubmitted] = createSignal(false)

  const isValid = createMemo(() =>
    form.email.includes('@') && form.password.length >= 8
  )

  const handleSubmit = async (e: Event) => {
    e.preventDefault()
    const result = schema.safeParse({ email: form.email, password: form.password })
    if (!result.success) {
      setForm('errors', result.error.flatten().fieldErrors)
      return
    }
    await submitToApi(result.data)
    setSubmitted(true)
  }

  return (
    <form onSubmit={handleSubmit}>
      <input value={form.email} onInput={(e) => setForm('email', e.currentTarget.value)} />
      {form.errors.email && <span>{form.errors.email}</span>}
      <input type="password" value={form.password} onInput={(e) => setForm('password', e.currentTarget.value)} />
      <button type="submit" disabled={!isValid()}>Submit</button>
    </form>
  )
}

Data Fetching with createResource

function UserProfile() {
  const params = useParams()
  const [user, { mutate, refetch }] = createResource(() => params.id, fetchUser)

  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Show when={user()} fallback={<div>User not found</div>}>
        <h1>{user().name}</h1>
        <p>{user().email}</p>
        <button onClick={() => mutate({ ...user()!, name: 'Updated' })}>Optimistic Update</button>
        <button onClick={refetch}>Refresh</button>
      </Show>
    </Suspense>
  )
}

Context Provider Pattern

const ThemeContext = createContext<{ theme: () => string; toggle: () => void }>()

function ThemeProvider(props: { children: any }) {
  const [theme, setTheme] = createSignal('light')
  const toggle = () => setTheme(t => t === 'light' ? 'dark' : 'light')
  return (
    <ThemeContext.Provider value={{ theme, toggle }}>
      {props.children}
    </ThemeContext.Provider>
  )
}

function ThemedButton() {
  const ctx = useContext(ThemeContext)!
  return <button class={`btn-${ctx.theme()}`} onClick={ctx.toggle}>Toggle Theme</button>
}

State Management Patterns

Local State with createSignal

const [count, setCount] = createSignal(0)
const [name, setName] = createSignal('')
const [items, setItems] = createSignal<string[]>([])

setCount(5)
setName('John')
setItems([...items(), 'new item'])
setItems(items => [...items, 'new item']) // functional update

Derived State with createMemo

const count = createSignal(0)
const doubled = createMemo(() => count() * 2)
const label = createMemo(() => count() > 10 ? 'High' : 'Low')
// doubled() and label() are cached until count() changes

Complex State with createStore

const [state, setState] = createStore({
  user: { profile: { name: 'Alice', email: 'alice@test.com' }, settings: { theme: 'dark' } },
  items: [] as Item[],
})
setState('user', 'profile', 'name', 'Bob') // path syntax
setState('items', items => [...items, newItem]) // functional

Shared State with Context

const AuthContext = createContext<{ user: () => User | null; login: (e: string, p: string) => Promise<void> }>()

function AuthProvider(props: { children: any }) {
  const [user, setUser] = createSignal<User | null>(null)
  const login = async (email: string, password: string) => {
    const u = await api.login(email, password)
    setUser(u)
  }
  return <AuthContext.Provider value={{ user, login }}>{props.children}</AuthContext.Provider>
}

Server State with createResource

const [data, { mutate, refetch }] = createResource(source, fetcher)
// source: reactive signal — refetches when source changes
// fetcher: async function that returns data
// mutate: optimistic update without refetch
// refetch: force re-fetch

Performance Optimization

No VDOM Overhead

SolidJS skips the virtual DOM entirely. The compiler produces direct DOM manipulation code:

// Input
<p>Count: {count()}</p>
// Compiled output (conceptual):
// textNode.data = `Count: ${count()}`

No diffing, no reconciliation, no component-level re-renders.

Granular Updates

When a signal changes, only the specific DOM nodes reading that signal update. A list of 1000 items updating one item updates exactly one DOM node — not the list, not the parent, not the component.

createStore Proxy Cost

createStore wraps objects in proxies for deep tracking. For very large arrays (10K+), proxy overhead can be significant. Use createSignal with immutable updates for performance-critical large lists.

Bundle Size

  • SolidJS runtime: ~8KB gzipped (vs React ~45KB, Vue ~30KB).
  • No JSX runtime needed — compiled away at build time.
  • Lazy imports via lazy() for route-level code splitting.

createEffect Best Practices

  • Effects run after DOM paint — safe to access DOM elements
  • Use onCleanup for subscriptions, intervals, listeners
  • Keep effects focused on one concern
  • Avoid reading signals in effects that don't need tracking (use untrack)

Build & Bundle Considerations

Vite Configuration

// vite.config.ts
import { defineConfig } from 'vite'
import solidPlugin from 'vite-plugin-solid'

export default defineConfig({
  plugins: [solidPlugin()],
  build: {
    target: 'esnext',
    polyfillDynamicImport: false,
  },
})

Build Commands

npm run build    # Production build to dist/
npm run dev      # Dev server with HMR

Code Splitting

import { lazy } from 'solid-js'
const Home = lazy(() => import('./pages/Home'))
const Users = lazy(() => import('./pages/Users'))

<Router>
  <Route path="/" component={Home} />
  <Route path="/users" component={Users} />
</Router>

SolidJS with TypeScript

  • Use Component<Props> type for component definitions
  • Signal types inferred automatically
  • Use JSX.Element for children/rendered content type

Testing Strategies

Unit Testing Signals

import { createSignal, createMemo, createEffect } from 'solid-js'
import { describe, it, expect } from 'vitest'

describe('signals', () => {
  it('createSignal works', () => {
    const [count, setCount] = createSignal(0)
    expect(count()).toBe(0)
    setCount(5)
    expect(count()).toBe(5)
  })

  it('createMemo caches derived values', () => {
    const [count, setCount] = createSignal(0)
    const doubled = createMemo(() => count() * 2)
    expect(doubled()).toBe(0)
    setCount(5)
    expect(doubled()).toBe(10)
  })
})

Testing Components

// __tests__/Counter.test.tsx
import { render, screen, fireEvent } from 'solid-testing-library'
import { describe, it, expect } from 'vitest'
import Counter from './Counter'

describe('Counter', () => {
  it('renders initial count', () => {
    render(() => <Counter initial={5} />)
    expect(screen.getByText('5')).toBeDefined()
  })

  it('increments on click', async () => {
    render(() => <Counter />)
    const btn = screen.getByRole('button')
    fireEvent.click(btn)
    expect(screen.getByText('1')).toBeDefined()
  })
})

E2E Testing

// e2e/app.spec.ts
import { test, expect } from '@playwright/test'

test('loads home page', async ({ page }) => {
  await page.goto('/')
  await expect(page.locator('h1')).toBeVisible()
})

Migration Patterns

React to SolidJS

// React: useState + useEffect + useMemo
const [count, setCount] = useState(0)
const doubled = useMemo(() => count * 2, [count])
useEffect(() => { document.title = `Count: ${count}` }, [count])

// SolidJS: createSignal + createMemo + createEffect
const [count, setCount] = createSignal(0)
const doubled = createMemo(() => count() * 2)
createEffect(() => { document.title = `Count: ${count()}` })
ReactSolidJS
useStatecreateSignal
useReducercreateStore with manual actions
useEffectcreateEffect
useMemocreateMemo
useCallback$() or module-level function
useContext + createContextcreateContext
useRefcreateSignal (for DOM) or ref prop

Anti-Patterns

Destructuring Signals

// Anti-pattern: destructuring loses tracking
const { count, setCount } = useCounter()
// <p>{count}</p> — never updates

// Correct: keep as signal
const count = useCounter()
// <p>{count()}</p>

createEffect for Derived State

// Anti-pattern: effect for computation
createEffect(() => { setDoubled(count() * 2) })

// Correct: memo
const doubled = createMemo(() => count() * 2)

Conditional createSignal

// Anti-pattern: signal inside condition
if (condition) { const [x] = createSignal(0) }

// Correct: always at top level
const [x] = createSignal(0)

Forgetting onCleanup

// Anti-pattern: no cleanup
createEffect(() => {
  const interval = setInterval(tick, 1000)
  // memory leak!
})

// Correct
createEffect(() => {
  const interval = setInterval(tick, 1000)
  onCleanup(() => clearInterval(interval))
})

Common Pitfalls

Pitfall 1: Destructuring Signals

// Wrong — destructuring breaks reactivity tracking
const [count, setCount] = createSignal(0)
const doubled = () => count() * 2 // trackable

// Wrong in JSX:
// <p>{count}</p> — count is a function, not the value
// Correct:
// <p>{count()}</p>

Signals are getter functions. Always call them as count() inside reactive scopes.

Pitfall 2: Using createEffect for Derived State

// Wrong — effect for computation
createEffect(() => { setDoubled(count() * 2) })
// Correct — memo for derived values
const doubled = createMemo(() => count() * 2)

Pitfall 3: Conditional Hook Calls

Unlike React, SolidJS components run once. You cannot call createSignal conditionally or inside loops:

// Wrong — signal created conditionally
if (condition) { const [x] = createSignal(0) }
// Correct — signal at top level
const [x] = createSignal(0)

Pitfall 4: Forgetting onCleanup

Effects and event listeners need cleanup to prevent memory leaks:

createEffect(() => {
  const interval = setInterval(tick, 1000)
  onCleanup(() => clearInterval(interval))
})

Pitfall 5: Overusing createStore for Simple Values

createStore adds proxy overhead. For single primitive values or simple objects, createSignal is more efficient and easier to use.

Compared With

SolidJS vs React

AspectSolidJSReact
RenderingNo VDOM, direct DOM updatesVDOM diffing
ReactivityFine-grained signalsComponent-level re-render
Component executionOnceEvery render cycle
StatecreateSignal/createStoreuseState/useReducer
Derived statecreateMemouseMemo
Side effectscreateEffectuseEffect
Bundle size~8KB (no runtime)~45KB + react-dom
Learning curveModerate (signals pattern)High (hooks rules, deps)

SolidJS vs Vue

Vue uses a proxy-based reactivity system similar to SolidJS's stores. SolidJS compiles JSX to discrete DOM operations, while Vue uses a virtual DOM. SolidJS is generally faster but Vue has a larger ecosystem.

SolidJS vs Svelte

Both compile to fine-grained DOM updates. Svelte uses template syntax with magic assignments; SolidJS uses JSX with explicit function calls for signals. SolidJS feels more like React; Svelte feels more like vanilla HTML.

Ecosystem & Tooling

Core Packages

PackagePurpose
solid-jsCore framework (signals, stores, components)
@solidjs/routerFile-like routing with signals
@solidjs/startMeta-framework (SSR, server functions)
solid-mdxMDX support for SolidJS

Tools

  • Solid VS Code Extension — Syntax highlighting, IntelliSense for signals and JSX.
  • Vite Plugin Solid — Official Vite plugin with HMR.
  • Solid DevTools — Chrome extension for inspecting signals and component graph.
  • solid-devtools — CLI-based dev tools for reactivity inspection.

UI Libraries

  • Solid UI — Component library for SolidJS.
  • Kobalte — Headless UI primitives (port of Radix UI).
  • Hope UI — Chakra UI-inspired component library.
  • Mantine Solid — Mantine port for SolidJS.

Community

  • Docs: solidjs.com
  • GitHub: github.com/solidjs/solid
  • Discord: discord.gg/solidjs
  • Playground: playground.solidjs.com

Workflow

Step 1: Reactivity Model

const [count, setCount] = createSignal(0)
const doubled = createMemo(() => count() * 2)
createEffect(() => console.log(`Count: ${count()}`))

setCount(5)
setCount(p => p + 1)

Signals are lazy — nothing recomputes until a signal is read inside a tracking scope.

Step 2: Component Model

function Counter(props: { initial?: number }) {
  const [count, setCount] = createSignal(props.initial ?? 0)
  return <button onClick={() => setCount(c => c + 1)}>{count()}</button>
}

Components run once. JSX compiles to document.createElement calls. Only signal expressions re-execute — not the whole component.

Step 3: Control Flow

<Show when={user()} fallback={<p>Loading...</p>}>
  <p>Welcome {user().name}</p>
</Show>

<For each={items()}>{(item, i) => <li>{i()}: {item.name}</li>}</For>

<Switch fallback={<p>Unknown</p>}>
  <Match when={tab() === 'info'}><Info /></Match>
  <Match when={tab() === 'settings'}><Settings /></Match>
</Switch>

<Index each={letters()}>{(letter, i) => <span>{i()}: {letter()}</span>}</Index>

Step 4: State Management with createStore

const [state, setState] = createStore({ user: { name: 'Alice' }, items: [] })
setState('user', 'name', 'Bob')
setState('items', items => [...items, { id: 1 }])

const mutable = createMutable({ count: 0 })
mutable.count++

Use createStore for complex state. Prefer path syntax over destructuring stores.

Step 5: Resources and Suspense

const [user] = createResource(() => params.id, fetchUser)

<Suspense fallback={<p>Loading user...</p>}>
  <p>{user()?.name}</p>
</Suspense>

Resources automatically track dependencies and refetch when params change.

Step 6: Routing

import { Router, Route } from '@solidjs/router'
import { lazy } from 'solid-js'

const Home = lazy(() => import('./pages/Home'))
const Users = lazy(() => import('./pages/Users'))

<Router>
  <Route path="/" component={Home} />
  <Route path="/users" component={Users} />
  <Route path="/users/:id" component={lazy(() => import('./pages/UserDetail'))} />
</Router>

Route params are signals: const params = useParams(). Prefetch with <Link>.

Rules

  • No hooks rules — components run once. Don't call signals conditionally.
  • JSX is compiled, not interpreted. No dynamic component creation.
  • Destructure signals with care — use props.name as () => props.name.
  • Use createMemo for derived values. Never use createEffect for computing values.
  • Solid is compile-time optimized — trust the compiler.
  • createStore path syntax for targeted updates. Avoid replacing entire objects.
  • Always call signals as functions (count()) inside reactive contexts.
  • onCleanup inside createEffect for subscriptions, intervals, event listeners.
  • Use createResource for async data with Suspense boundaries.
  • Avoid createMutable in production — prefer createStore for predictability.

References

  • references/solid-components.md — SolidJS Components — JSX, Control Flow, Context, Portals, Lifecycle
  • references/solid-reactivity.md — SolidJS Reactivity — Signals, Memos, Effects, Stores, Batching
  • references/solidjs-component-architecture.md — SolidJS Component Architecture
  • references/solidjs-optimization.md — SolidJS Optimization Patterns
  • references/solidjs-reactivity.md — SolidJS Reactivity Patterns
  • references/solidjs-signal-patterns.md — SolidJS Signal Patterns
  • references/solidjs-reactivity-deep-dive.md — SolidJS Reactivity Deep Dive
  • references/solidjs-composables-primitives.md — SolidJS Composables and Primitives

Handoff

No artifact produced. Next skill: frontend-solidjs-patterns for data fetching, forms, composition, and animation. Carry forward: signal patterns, store usage, control flow conventions.

Gespeicherte Skills