← All posts
·18 min read

Claude Code with Solid.js: Signals, Stores, and Fine-Grained Reactivity

Claude CodeSolid.jsFrontendReactive
Claude Code with Solid.js: Signals, Stores, and Fine-Grained Reactivity

Why Solid.js without CLAUDE.md gets React-rewritten and loses reactivity

Solid.js is the fastest mainstream reactive framework for the browser, and the reason it is fast is also the reason Claude Code regularly breaks it. Solid has no virtual DOM. Reactivity is not driven by component re-renders. It is driven by signal reads inside a reactive context: when you call count() inside JSX or inside a createEffect, Solid registers a subscription. When the signal updates, only that exact piece of DOM or that exact effect re-runs, nothing else.

Claude Code was trained on a corpus that is overwhelmingly React. The mental model it reaches for by default is: state triggers a re-render, the whole component function re-executes, and the virtual DOM diff figures out what changed. In Solid that model does not exist. The component function runs exactly once, at mount. After that, only reactive computations (effects, memos, JSX expressions) re-run when their signal dependencies change. If Claude writes const count = createSignal(0) and then reads count (without the call) in JSX, the JSX displays the function reference, not the value, and never updates.

The pattern failures compound. Claude reaches for useState imports that do not exist in Solid. It writes items.map(item => <Item />) inside JSX, which runs once at mount and never updates when items changes because the map is outside a reactive scope. It adds dependency arrays to createEffect that Solid ignores. It writes ternary expressions for conditional rendering instead of <Show> components, which prevents Solid from tracking the condition as a reactive dependency and doing targeted DOM surgery.

None of these bugs are immediately visible. The component renders on the first pass. The failure appears the moment state changes and nothing in the UI updates, or the list re-renders the wrong items, or an effect runs with stale values. Debugging fine-grained reactivity failures without understanding Solid's ownership model takes significant time.

This guide covers the CLAUDE.md configuration that anchors Claude Code to Solid's actual model: signals read via function calls, derived values via createMemo, effects that auto-track without dependency arrays, nested state via createStore with path-based updates, async data via createResource, and control flow via Show, For, and Switch. For a comparison with a component model that has a VDOM, Claude Code with React covers the React-specific CLAUDE.md rules. For a compiler-based reactive system with a different set of constraints, Claude Code with Svelte is a useful contrast.

The Solid CLAUDE.md template

The CLAUDE.md at your project root is read at the start of every Claude Code session. For a Solid.js project it must declare: the Solid version and TypeScript configuration, the signal read convention (always () call), the banned React patterns, the control flow components that replace JSX map and ternaries, the store update API, the resource pattern for async, and the hard rules that block the patterns Claude generates most often without guidance.

# Solid.js project rules

## Stack
- Solid.js 1.8.x, TypeScript 5.x strict
- Vite 5.x with vite-plugin-solid
- SolidStart 1.x for SSR routes (if applicable)
- @solidjs/router 0.13.x for client-side routing

## Core primitive: signals
- Import: import { createSignal } from 'solid-js'
- Declaration: const [count, setCount] = createSignal(0)
- Reading: ALWAYS call the getter, count() NOT count
- Writing: setCount(1) or setCount(prev => prev + 1)
- NEVER destructure signal values: const { count } = ... is FORBIDDEN
- Calling count() inside JSX or a reactive scope (createEffect, createMemo)
  registers a subscription. This is how Solid tracks reactivity.
- Calling count() OUTSIDE a reactive scope reads the value once with no subscription

## Derived values: createMemo
- Import: import { createMemo } from 'solid-js'
- Declaration: const doubled = createMemo(() => count() * 2)
- Usage: doubled(). Same () call convention as signals.
- NEVER compute derived values inline in JSX: {count() * 2} is OK for simple cases
  but createMemo caches and memoises for expensive computations
- NEVER use useMemo. This is React, not Solid.

## Effects: createEffect
- Import: import { createEffect } from 'solid-js'
- Declaration: createEffect(() => { console.log(count()) })
- Solid auto-tracks every signal called inside the effect. NO dependency array needed.
- NEVER add a dependency array: createEffect(() => {...}, [count]) is WRONG
- NEVER put JSX or DOM creation inside createEffect
- createEffect runs after render, use it for side effects (logging, analytics, DOM sync)
- createEffect re-runs whenever any signal read inside it changes

## Cleanup in effects
- Return a cleanup function from createEffect for subscriptions and timers:
  createEffect(() => {
    const id = setInterval(() => setTick(t => t + 1), 1000)
    return () => clearInterval(id)
  })
- onCleanup() is the alternative: import { onCleanup } from 'solid-js'

## Stores: nested and complex state
- Import: import { createStore } from 'solid-js/store'
- Declaration: const [state, setState] = createStore({ user: { name: '', age: 0 } })
- Reading: state.user.name (no () call; stores use Proxy, not functions)
- Updating nested paths: setState('user', 'name', 'Alice')
- Updating via function: setState('user', 'name', prev => prev.toUpperCase())
- Array update: setState('items', produce(items => { items.push({ id: 1 }) }))
- NEVER spread to update: setState({ ...state, user: { name: 'Alice' } }) loses reactivity
- Import produce from 'solid-js/store' for immer-style mutations

## Resources: async data
- Import: import { createResource } from 'solid-js'
- Declaration: const [data, { refetch }] = createResource(fetchUser)
- With source signal: const [data] = createResource(userId, id => fetchUser(id))
  and the resource refetches automatically when userId() changes
- data() returns undefined while loading
- data.loading returns true while pending
- data.error returns the error if rejected
- Wrap resource reads in <Show> or <Suspense> to handle loading and error states

## Control flow: MANDATORY patterns
- Lists: ALWAYS use <For each={items()}>{(item, index) => <div>{item.name}</div>}</For>
  NEVER items().map(item => <div>{item.name}</div>). .map() is not reactive.
- Conditionals: ALWAYS use <Show when={isLoggedIn()} fallback={<Login />}><Dashboard /></Show>
  NEVER ternaries for component-level conditionals. Ternaries prevent targeted DOM updates.
- Switch: <Switch fallback={<NotFound />}><Match when={route() === 'home'}><Home /></Match></Switch>
- Index (for arrays where values change but positions don't):
  <Index each={items()}>{(item, index) => <div>{item().name}</div>}</Index>
  Note: in Index, item is a signal (item()), in For, item is the value directly

## Component lifecycle
- Component function runs ONCE at mount. This is not a render function.
- Do not put reactive logic in the component body outside signal/effect/memo declarations
- Reactive expressions belong in: JSX templates, createEffect, createMemo, createResource
- onMount: import { onMount } from 'solid-js', runs after first render, not reactive
- onCleanup: runs when component unmounts or parent effect re-runs

## Hard rules
- NEVER import useState, useEffect, useMemo, useRef from anywhere. These are React APIs.
- NEVER read a signal without (): const val = count is a bug
- NEVER use .map() in JSX for reactive lists. Use <For> instead.
- NEVER add a dependency array to createEffect
- NEVER use ternaries for conditional component rendering. Use <Show> instead.
- NEVER setState via spread on stores. Use path-based updates.
- ALWAYS call signals and memos with () to read their value
- ALWAYS use <Suspense> around components that read resources

Three rules here prevent the majority of reactivity failures Claude generates without them.

The no-() bug rule is the most critical. In Solid, count is a getter function. Writing <div>{count}</div> renders the string representation of the function itself, and the div never updates. Writing <div>{count()}</div> subscribes to the signal and re-renders that text node when count changes. Claude generates the missing () version at a rate that correlates directly with how much React code was in its training context for the feature being built. The rule forces the correct syntax.

The no-map-in-JSX rule matters because .map() in JSX is a one-time execution. At mount, Solid runs the component once, the .map() produces DOM nodes, and those nodes are inserted. When items (the signal) updates, Solid does not re-run the component. The .map() result is already inert. <For each={items()}> is a reactive computation: it tracks items(), diffs the array, and updates only the DOM nodes that correspond to changed items. On large lists the performance difference is significant. On any list it is the difference between working and broken.

The no-dependency-array rule is the most common import from React habits. createEffect(() => {...}, [count]) in Solid does not throw an error. The second argument is silently ignored. The effect auto-tracks every signal called inside it, regardless of the array. The rule prevents the false confidence that the dependency array is doing something and prevents the confusion when the effect runs more or less often than expected.

Signals vs React state: what actually changes

The key mental model shift when moving from React to Solid is that state does not trigger re-renders. In React, useState causes the component function to re-execute. Every hook re-runs. Every derived value recomputes. The virtual DOM diffs the output. In Solid, createSignal creates a reactive primitive. Reading it in a reactive context subscribes that context to changes. The component function does not re-run.

This has practical consequences for how Claude should structure components.

In React:

// React (Claude's default pattern)
function Counter() {
  const [count, setCount] = useState(0)
  const doubled = count * 2  // recomputes on every render, fine in React
  return <div onClick={() => setCount(c => c + 1)}>{count} (doubled: {doubled})</div>
}

In Solid:

// Solid (what CLAUDE.md enforces)
import { createSignal, createMemo } from 'solid-js'

function Counter() {
  const [count, setCount] = createSignal(0)
  const doubled = createMemo(() => count() * 2) // reactive memo, runs only when count() changes
  return <div onClick={() => setCount(c => c + 1)}>{count()} (doubled: {doubled()})</div>
}

The component body of Counter in Solid runs once. createSignal and createMemo are called once to register the reactive graph. The JSX references count() and doubled(), which are reactive reads that update their specific DOM nodes when the signal changes. The onClick setter updates the signal, Solid propagates the change through the reactive graph, and only the two text nodes re-render.

Without CLAUDE.md enforcing this, Claude writes const doubled = count() * 2 directly in the component body. This evaluates once at mount (because the component runs once) and never updates. It looks identical to working React code and produces a broken Solid component.

createMemo for derived values

createMemo is Solid's equivalent of a computed property. It creates a reactive computation that caches its result and only re-runs when a signal it reads inside changes.

Add a memo section to CLAUDE.md with worked examples:

## createMemo patterns

### Basic derived value
const [price, setPrice] = createSignal(100)
const [quantity, setQuantity] = createSignal(3)
const total = createMemo(() => price() * quantity())
// total() returns 300, updates when price or quantity changes

### Expensive computation (memo prevents redundant recalculation)
const [items, setItems] = createStore([])
const sortedItems = createMemo(() =>
  [...items].sort((a, b) => a.price - b.price)
)
// sortedItems() re-sorts only when items store changes

### Memo reading another memo (fine, Solid handles the dependency graph)
const taxRate = createMemo(() => total() > 1000 ? 0.2 : 0.1)
const taxAmount = createMemo(() => total() * taxRate())

### NEVER do this (not reactive, runs once at mount)
const total = price() * quantity()  // stale after first render

### NEVER do this (React import, does not exist in Solid)
const total = useMemo(() => price() * quantity(), [price, quantity])

The distinction between createMemo and an inline JSX expression matters at scale. {price() * quantity()} in JSX works correctly for a single usage: that text node re-renders when either signal changes. But if the same computation appears in three places in the template, each occurrence runs independently. createMemo runs the computation once and shares the cached result with all three consumers. On complex component trees this prevents redundant work.

createEffect without dependency arrays

createEffect runs a side effect whenever any signal it reads inside changes. Solid builds the dependency graph at runtime by tracking which signals are accessed during execution. There is no static analysis, no lint rule, and no array to maintain.

Add an effects section to CLAUDE.md:

## createEffect patterns

### Basic effect (auto-tracks count)
createEffect(() => {
  document.title = `Count: ${count()}`
})

### Effect with cleanup (subscription example)
createEffect(() => {
  const ws = new WebSocket(`wss://api.example.com/stream/${userId()}`)
  ws.onmessage = e => setMessages(msgs => [...msgs, JSON.parse(e.data)])
  return () => ws.close()  // cleanup runs before next effect execution or on unmount
})

### Effect reading from store
createEffect(() => {
  console.log('User changed:', state.user.name)
})

### Conditional read inside effect (CAREFUL: condition changes tracking)
createEffect(() => {
  if (isEnabled()) {
    // count() only tracked when isEnabled() is true
    console.log(count())
  }
})

### FORBIDDEN: dependency array (ignored by Solid, misleading)
createEffect(() => { console.log(count()) }, [count])

### FORBIDDEN: JSX inside effect (breaks Solid's ownership model)
createEffect(() => {
  return <div>{count()}</div>  // DO NOT do this
})

### onMount vs createEffect
// onMount: runs once after first render, NOT reactive
onMount(() => {
  fetchInitialData().then(setData)
})
// createEffect: reactive, re-runs when signals inside change
createEffect(() => {
  if (userId()) fetchUserData(userId()).then(setData)
})

The conditional tracking behaviour is a nuance that Claude will not get right without explicit documentation. When isEnabled() is false, Solid does not execute the inner branch, so count() is never called, and count is not in the effect's dependency set. When isEnabled() becomes true, the effect re-runs (because isEnabled is tracked), calls count(), adds it to the dependency set, and from that point tracks both signals. This is correct behaviour and is the mechanism that lets Solid avoid subscribing to signals that are not currently relevant. Documenting it prevents Claude from "fixing" the conditional by hoisting all reads to the top of the effect.

Stores for nested and object state

Solid signals are designed for primitive values. For objects and arrays, createStore provides a Proxy-based reactive system where individual nested properties are tracked independently. Updating state.user.name re-runs only the computations that read state.user.name, not anything that reads state.user.age or state.settings.

Add a stores section to CLAUDE.md:

## createStore patterns

### Declaration
import { createStore, produce } from 'solid-js/store'

const [state, setState] = createStore({
  user: { name: '', email: '', role: 'viewer' },
  settings: { theme: 'dark', notifications: true },
  items: [] as Item[],
})

### Reading (no () call; Proxy-based, not function-based)
state.user.name
state.settings.theme
state.items.length

### Updating nested path
setState('user', 'name', 'Alice')
setState('settings', 'theme', 'light')

### Updating with function (receives current value)
setState('user', 'name', prev => prev.trim())

### Updating array item at index
setState('items', 0, 'completed', true)

### Updating multiple fields at once
setState('user', { name: 'Alice', email: 'alice@example.com' })

### Adding to array
setState('items', produce(items => {
  items.push({ id: Date.now(), text: 'New item', completed: false })
}))

### Filtering array
setState('items', items => items.filter(item => !item.completed))

### Reconcile (for replacing with server data; preserves reactive references)
import { reconcile } from 'solid-js/store'
setState('items', reconcile(newItemsFromServer))

### FORBIDDEN: spread update loses granular reactivity
setState({ ...state, user: { ...state.user, name: 'Alice' } })

The spread update failure is subtle. setState({ ...state, user: { ...state.user, name: 'Alice' } }) does update the store. But it creates new object references for user and for the top-level store. Solid's Proxy-based tracking sees a new user object and invalidates all computations that read any user property, even those that did not change. Path-based updates (setState('user', 'name', 'Alice')) update only the name property, and only the computations tracking state.user.name re-run.

createResource for async data

createResource is Solid's primitive for async data fetching. It returns a signal-like accessor with loading and error properties. When the source signal changes, the resource automatically re-fetches.

## createResource patterns

### Basic fetch (no source signal, fetches once on mount)
const [profile] = createResource(() => fetch('/api/profile').then(r => r.json()))

### With source signal (re-fetches when userId changes)
const [userId] = createSignal(1)
const [user] = createResource(userId, id =>
  fetch(`/api/users/${id}`).then(r => r.json())
)

### Accessing resource data
user()           // the data (undefined while loading)
user.loading     // true while pending
user.error       // the error if rejected (undefined otherwise)
user.state       // 'unresolved' | 'pending' | 'ready' | 'refreshing' | 'errored'

### With refetch
const [data, { refetch, mutate }] = createResource(fetchData)
// refetch() re-runs the fetcher
// mutate(newValue) updates the value without a fetch

### In JSX with Suspense and ErrorBoundary
import { Suspense } from 'solid-js'

<ErrorBoundary fallback={err => <div>Error: {err.message}</div>}>
  <Suspense fallback={<Spinner />}>
    <UserProfile user={user()} />
  </Suspense>
</ErrorBoundary>

### FORBIDDEN: reading resource in non-Suspense context without loading check
<div>{user().name}</div>  // throws if user() is undefined while loading

The source-signal pattern is the feature that replaces the React useEffect + dependency array fetch pattern entirely. In React, you write useEffect(() => { fetch(url).then(...) }, [userId]). In Solid, createResource(userId, id => fetch(...)) re-fetches whenever userId() changes, with loading and error states managed automatically. Claude will write the useEffect version by default. The CLAUDE.md pattern shows the idiomatic Solid replacement.

Control flow: Show, For, Switch, Index

Solid's control flow components are not syntactic sugar. They are reactive computations that track the condition or collection and update only the affected DOM when it changes. Using JSX alternatives (ternaries, .map()) removes that tracking and produces components that do not update correctly.

Add a control flow section to CLAUDE.md with the patterns Claude gets wrong:

## Control flow components

### Show: conditional rendering
import { Show } from 'solid-js'

// Correct
<Show when={isLoggedIn()} fallback={<LoginButton />}>
  <UserMenu />
</Show>

// Correct: keyed Show (destroys and recreates child when key changes)
<Show when={user()} keyed>
  {user => <UserCard name={user.name} />}
</Show>

// FORBIDDEN: ternary for component-level conditionals
{isLoggedIn() ? <UserMenu /> : <LoginButton />}

### For: reactive lists
import { For } from 'solid-js'

// Correct
<For each={items()}>
  {(item, index) => (
    <li data-index={index()}>
      {item.name}
    </li>
  )}
</For>

// FORBIDDEN: .map() is not reactive
{items().map(item => <li>{item.name}</li>)}

### Switch + Match: multi-branch conditional
import { Switch, Match } from 'solid-js'

<Switch fallback={<NotFound />}>
  <Match when={page() === 'home'}><Home /></Match>
  <Match when={page() === 'settings'}><Settings /></Match>
  <Match when={page() === 'profile'}><Profile /></Match>
</Switch>

### Index: when item position is stable but value changes
import { Index } from 'solid-js'

// item is a signal in Index (call item() to read), index is a number
<Index each={items()}>
  {(item, index) => <li>{item().name} at position {index}</li>}
</Index>

// Use For when items are added/removed (keyed by identity)
// Use Index when the array length is stable but values update in place

### Dynamic: component from variable
import { Dynamic } from 'solid-js/web'
<Dynamic component={components[type()]} {...props()} />

### Portal: render outside component tree
import { Portal } from 'solid-js/web'
<Portal mount={document.getElementById('modal-root')!}>
  <Modal />
</Portal>

The For vs Index distinction matters for list update performance. <For> keys items by identity: when an item object is removed, its DOM node is destroyed; when a new one is added, a new DOM node is created. <Index> keys by position: when the value at index 2 changes, only the signal for position 2 is updated, and the DOM node at that position updates in place. For editable fields in a fixed-length list, <Index> avoids unnecessary DOM destruction and recreation.

SolidStart for SSR and file-based routing

SolidStart 1.x brings server-side rendering and file-based routing to Solid.js. The routing convention, server functions, and data loading patterns are different enough from client-only Solid that they need their own CLAUDE.md section.

## SolidStart rules

## File-based routing (src/routes/)
- src/routes/index.tsx           → /
- src/routes/users/index.tsx     → /users
- src/routes/users/[id].tsx      → /users/:id (dynamic segment)
- src/routes/users/[...all].tsx  → /users/* (catch-all)

## Route component structure
import { RouteDefinition, useParams } from '@solidjs/router'
import { createResource } from 'solid-js'

// Route preload (parallel data fetching before render)
export const route = {
  preload: ({ params }) => getUser(params.id),
} satisfies RouteDefinition

export default function UserPage() {
  const params = useParams()
  const [user] = createResource(() => params.id, getUser)
  return <Show when={user()}>{u => <UserCard user={u} />}</Show>
}

## Server functions ("use server")
import { action, query } from '@solidjs/router'

// Query: read data on server, cached by default
const getUser = query(async (id: string) => {
  "use server"
  return db.user.findUnique({ where: { id } })
}, 'getUser')

// Action: mutate data on server
const updateUser = action(async (formData: FormData) => {
  "use server"
  const name = formData.get('name') as string
  return db.user.update({ where: { id: formData.get('id') as string }, data: { name } })
})

## Hard rules for SolidStart
- "use server" directive goes INSIDE the function body, as the first statement
- query() functions are deduplicated and cached. Do NOT call them in createEffect.
- action() functions return redirect() or throw to handle navigation on success/error
- Server-only code (db, env secrets) belongs in query/action bodies, never in JSX or client signals
- preload runs on server for SSR and on client for navigation. Keep it light.

The "use server" placement matters and is different from Next.js conventions. In SolidStart, the directive is a string inside the function body, not a file-level directive. Claude will sometimes write 'use server' at the top of the file (Next.js style) or miss it entirely and expose database calls to the client bundle. The CLAUDE.md rule prevents both failure modes.

Common gotchas Claude introduces without CLAUDE.md

Beyond the core patterns, several failure modes appear repeatedly in Claude-generated Solid code when there is no guidance file.

Signal read outside reactive context. Claude sometimes writes:

const currentCount = count()  // reads once, no subscription
createEffect(() => {
  console.log(currentCount)  // logs the stale value forever
})

count() is called outside the createEffect, so no subscription is created for the effect. The effect runs once with the initial value and never again. The fix is to call count() inside the effect body.

Derived signals passed as props without ().

// Claude generates (broken)
<ChildComponent value={count} />  // passes the signal function, not its value

// Correct
<ChildComponent value={count()} />  // passes reactive value

// Also correct: pass the getter for lazy reading in child
<ChildComponent value={count} />  // only if ChildComponent is designed to receive and call signal()

Passing a signal getter as a prop is valid if the child component calls props.value() to read it reactively. Passing it when the child expects a plain value breaks silently. CLAUDE.md should declare the convention for the project.

Store updates that return early before modifying nested state. Solid's path-based store setter uses a variadic path: setState('items', 0, 'completed', true). Claude sometimes generates: setState({ items: state.items.map(... ) }), which replaces the entire items array with a new reference and triggers a full list re-render instead of a targeted update.

Importing from solid-js vs solid-js/web. createSignal, createEffect, createMemo, createResource, createStore, Show, For, Switch, Match all come from 'solid-js'. render, Dynamic, Portal, HydrationScript come from 'solid-js/web'. Claude will sometimes import from the wrong module, producing a runtime error that is not immediately obvious from the import path.

For the broader question of how component-based frameworks differ in their Claude Code configuration, Claude Code with Vue covers the Options API vs Composition API decision and the reactive system differences that affect CLAUDE.md rules in a similar way.

Getting Claudify

The Solid.js CLAUDE.md template in this guide gives Claude Code the constraints it needs to generate idiomatic fine-grained reactive code: signals read with (), createMemo for derived values, dependency-array-free createEffect, path-based store updates, createResource for async data, and Show/For/Switch for all conditional and list rendering.

The template addresses the root cause of every common Claude failure in Solid projects. Without it, Claude defaults to the React mental model, produces components that render correctly once and stop updating, and generates bugs that require understanding Solid's ownership model to diagnose.

Claudify includes a Solid.js-specific CLAUDE.md template, pre-configured for all the signal, memo, effect, store, resource, and control flow patterns shown in this guide, with TypeScript strict mode and SolidStart variants included.

More like this

Ready to upgrade your Claude Code setup?

Get Claudify
Featured on Dofollow.Tools AI Toolz Dir Claudify - Featured on Startup Fame