SolidJS achieves React-level ergonomics with Svelte-level performance by compiling JSX to direct DOM operations — there’s no virtual DOM, no reconciliation, and no re-renders of components. Signals are reactive variables that notify only their exact dependents when they change. createMemo caches derived values. createStore provides nested reactive state. SolidStart adds file-based routing, server functions ("use server"), and streaming SSR on top. Claude Code generates Solid components, reactive store patterns, SolidStart server functions, and the routing configuration for high-performance Solid applications.
CLAUDE.md for SolidJS Projects
## SolidJS Stack
- Version: solid-js 1.9+, @solidjs/router 0.15+
- Framework: SolidStart 1.0 (file-based SSR + server functions)
- TypeScript: strict mode, solid-ts preset
- Styling: Tailwind CSS or CSS Modules
- State: createSignal (local), createStore (complex objects), Context (cross-component)
- Data fetching: createResource for async, Suspense for loading states
- Testing: vitest + @solidjs/testing-library
- NEVER use React hooks (useState, useEffect) — use Solid equivalents
Core Reactivity Primitives
// components/OrderDashboard.tsx — fine-grained signals
import {
createSignal, createMemo, createEffect,
For, Show, Switch, Match,
} from 'solid-js'
interface Order {
id: string
status: 'pending' | 'processing' | 'shipped' | 'delivered'
totalCents: number
customerName: string
createdAt: string
}
export function OrderDashboard(props: { orders: Order[] }) {
const [filter, setFilter] = createSignal<string>('all')
const [searchQuery, setSearchQuery] = createSignal('')
const [sortBy, setSortBy] = createSignal<'date' | 'amount'>('date')
// createMemo: only recomputes when filter, searchQuery, or sortBy changes
// (not on every unrelated signal update)
const filteredOrders = createMemo(() => {
let result = props.orders
if (filter() !== 'all') {
result = result.filter(o => o.status === filter())
}
const query = searchQuery().toLowerCase()
if (query) {
result = result.filter(o =>
o.customerName.toLowerCase().includes(query) ||
o.id.includes(query)
)
}
return [...result].sort((a, b) => {
if (sortBy() === 'date') {
return b.createdAt.localeCompare(a.createdAt)
}
return b.totalCents - a.totalCents
})
})
const totalRevenue = createMemo(() =>
filteredOrders()
.filter(o => o.status === 'delivered')
.reduce((sum, o) => sum + o.totalCents, 0)
)
// createEffect: runs when dependencies change (for side effects)
createEffect(() => {
document.title = `Orders (${filteredOrders().length})`
})
return (
<div class="p-6">
<div class="flex gap-4 mb-6">
<input
type="search"
placeholder="Search orders..."
value={searchQuery()}
onInput={e => setSearchQuery(e.currentTarget.value)}
class="border rounded px-3 py-2"
/>
<select
value={filter()}
onChange={e => setFilter(e.currentTarget.value)}
class="border rounded px-3 py-2"
>
<option value="all">All</option>
<option value="pending">Pending</option>
<option value="shipped">Shipped</option>
<option value="delivered">Delivered</option>
</select>
<button
onClick={() => setSortBy(s => s === 'date' ? 'amount' : 'date')}
class="btn btn-secondary"
>
Sort by {sortBy() === 'date' ? 'Amount' : 'Date'}
</button>
</div>
<p class="text-gray-500 mb-4">
{filteredOrders().length} orders · ${(totalRevenue() / 100).toFixed(2)} revenue
</p>
{/* For: efficient list rendering — only updates changed items */}
<For each={filteredOrders()} fallback={<p>No orders found.</p>}>
{(order) => (
<div class="border rounded-lg p-4 mb-3">
<div class="flex justify-between">
<div>
<span class="font-mono text-sm">{order.id}</span>
<p class="text-gray-700">{order.customerName}</p>
</div>
<div class="text-right">
<p class="font-semibold">${(order.totalCents / 100).toFixed(2)}</p>
{/* Show: conditional rendering without unmounting */}
<Show
when={order.status === 'delivered'}
fallback={<span class="text-orange-500">{order.status}</span>}
>
<span class="text-green-600">delivered</span>
</Show>
</div>
</div>
</div>
)}
</For>
</div>
)
}
createStore for Complex State
// stores/order-store.ts — reactive store with nested updates
import { createStore, produce, reconcile } from 'solid-js/store'
interface OrderStore {
orders: Record<string, Order>
selectedIds: Set<string>
loading: boolean
error: string | null
}
const [store, setStore] = createStore<OrderStore>({
orders: {},
selectedIds: new Set(),
loading: false,
error: null,
})
// Fine-grained updates — only affected subscribers rerun
function setOrderStatus(orderId: string, status: Order['status']) {
setStore('orders', orderId, 'status', status)
}
function selectOrder(id: string, selected: boolean) {
setStore('selectedIds', ids => {
const next = new Set(ids)
selected ? next.add(id) : next.delete(id)
return next
})
}
// Batch update with produce (immer-like)
async function fetchOrders() {
setStore('loading', true)
try {
const data = await api.getOrders()
setStore(
produce(s => {
s.loading = false
s.error = null
// Add all fetched orders
for (const order of data) {
s.orders[order.id] = order
}
})
)
} catch (err) {
setStore({ loading: false, error: (err as Error).message })
}
}
// Replace store with fresh data (reconcile preserves references)
function replaceOrders(newOrders: Order[]) {
const orderMap = Object.fromEntries(newOrders.map(o => [o.id, o]))
setStore('orders', reconcile(orderMap))
}
export { store, setOrderStatus, selectOrder, fetchOrders }
Context for Cross-Component State
// contexts/auth.tsx
import { createContext, useContext, createSignal, ParentProps } from 'solid-js'
interface AuthContextValue {
user: () => User | null
login: (email: string, password: string) => Promise<void>
logout: () => void
isAuthenticated: () => boolean
}
const AuthContext = createContext<AuthContextValue>()
export function AuthProvider(props: ParentProps) {
const [user, setUser] = createSignal<User | null>(null)
async function login(email: string, password: string) {
const u = await api.login(email, password)
setUser(u)
}
function logout() {
api.logout()
setUser(null)
}
return (
<AuthContext.Provider value={{
user,
login,
logout,
isAuthenticated: () => user() !== null,
}}>
{props.children}
</AuthContext.Provider>
)
}
export function useAuth() {
const ctx = useContext(AuthContext)
if (!ctx) throw new Error('useAuth must be used within AuthProvider')
return ctx
}
SolidStart Server Functions
// app.tsx (SolidStart entry) and routes
import { createAsync, query, action, redirect } from '@solidjs/router'
import { Suspense } from 'solid-js'
// Server query — runs on server, result serialized to client
const getOrders = query(async (status?: string) => {
"use server"
const db = await getDb()
return db.orders.findMany({
where: status ? { status } : undefined,
orderBy: { createdAt: 'desc' },
take: 20,
})
}, 'orders')
// Server action — handles mutations
const updateOrderStatus = action(async (formData: FormData) => {
"use server"
const orderId = formData.get('orderId') as string
const status = formData.get('status') as string
const db = await getDb()
await db.orders.update({ where: { id: orderId }, data: { status } })
return redirect(`/dashboard/orders/${orderId}`)
})
// Route component using server function
export default function OrdersPage() {
const orders = createAsync(() => getOrders()) // Suspense-aware
return (
<Suspense fallback={<div>Loading orders...</div>}>
<div>
<For each={orders()} fallback={<p>No orders</p>}>
{order => (
<div>
<span>{order.id}</span>
{/* Action form — works without JavaScript */}
<form action={updateOrderStatus} method="post">
<input type="hidden" name="orderId" value={order.id} />
<select name="status">
<option value="PROCESSING">Processing</option>
<option value="SHIPPED">Shipped</option>
</select>
<button type="submit">Update</button>
</form>
</div>
)}
</For>
</div>
</Suspense>
)
}
createResource for Async Data
// Async data loading with createResource
import { createResource, createSignal, Suspense, ErrorBoundary } from 'solid-js'
function OrderDetail(props: { orderId: string }) {
// createResource fetches and tracks async data
const [order, { refetch }] = createResource(
() => props.orderId, // Source signal — refetches when orderId changes
async (id) => {
const resp = await fetch(`/api/orders/${id}`)
if (!resp.ok) throw new Error(`Order ${id} not found`)
return resp.json()
}
)
return (
<ErrorBoundary fallback={(err) => <div>Error: {err.message}</div>}>
<Suspense fallback={<div class="animate-pulse">Loading...</div>}>
<Show when={order()}>
{o => (
<div>
<h2>Order {o().id}</h2>
<p>Status: {o().status}</p>
<p>Total: ${(o().totalCents / 100).toFixed(2)}</p>
<button onClick={refetch}>Refresh</button>
</div>
)}
</Show>
</Suspense>
</ErrorBoundary>
)
}
Performance: Why Solid is Fast
// Signals are NOT reevaluated on render — they ARE reactive atoms
// This code demonstrates the difference from React:
// React re-renders the ENTIRE component when state changes
// function ReactComponent() {
// const [count, setCount] = useState(0)
// return <div><span>{count}</span></div> // Full re-render on each click
// }
// Solid: ONLY the <span> DOM node updates — no component re-execution
function SolidComponent() {
const [count, setCount] = createSignal(0)
// This function runs ONCE — not on every update
console.log("Component function ran once")
return (
<div>
{/* Only updates the text node — zero extra work */}
<span>{count()}</span>
<button onClick={() => setCount(c => c + 1)}>+</button>
</div>
)
}
For the React alternative with a larger ecosystem and similar component patterns, see the React 19 guide for server components and hooks. For the Svelte 5 runes-based reactivity that achieves similar fine-grained updates through a compiler, the SvelteKit advanced guide covers Svelte’s reactive model. The Claude Skills 360 bundle includes SolidJS skill sets covering signals, stores, and SolidStart server functions. Start with the free tier to try SolidJS component generation.