Claude Code for Solid.js: Fine-Grained Reactivity Without Virtual DOM — Claude Skills 360 Blog
Blog / Development / Claude Code for Solid.js: Fine-Grained Reactivity Without Virtual DOM
Development

Claude Code for Solid.js: Fine-Grained Reactivity Without Virtual DOM

Published: July 5, 2026
Read time: 8 min read
By: Claude Skills 360

Solid.js achieves React-like developer experience with significantly better performance by eliminating the virtual DOM entirely. Components run once — Solid’s fine-grained reactivity tracks signal dependencies at compile time and updates only the exact DOM nodes that need updating. Claude Code understands Solid’s reactivity model well enough to avoid the common mistakes: creating reactive reads outside reactive contexts and over-destructuring store properties.

This guide covers Solid.js with Claude Code: signals, derived computations, stores, SolidStart, and the mental model that makes Solid work.

The Reactivity Model

Solid’s core insight: components are setup functions, not render functions.

// React: re-renders the entire function on every state change
function Counter() {
  const [count, setCount] = useState(0);
  console.log('rendered'); // Logs on every click
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}

// Solid: component runs ONCE, signals update DOM directly
function Counter() {
  const [count, setCount] = createSignal(0);
  console.log('setup'); // Logs ONCE
  // Only the text node `{count()}` updates on click — not the button
  return <button onClick={() => setCount(c => c + 1)}>{count()}</button>;
}
I keep getting "computations created outside a reactive root" warning.
What am I doing wrong?

Claude Code explains: signals must be read inside reactive contexts (components, createEffect, createMemo). Reading count() in a setTimeout or a plain function call loses reactivity.

// Wrong — signal read in non-reactive context
function fetchWithCount() {
  setTimeout(() => {
    console.log(count()); // Works once, but doesn't re-run when count changes
  }, 1000);
}

// Right — wrap in createEffect to track dependencies
createEffect(() => {
  setTimeout(() => {
    console.log(count()); // Logs current count, but won't re-track
  }, 1000);
});

// Better — read inside the reactive function
createEffect(() => {
  const value = count(); // Tracked dependency
  setTimeout(() => {
    console.log(value); // Snapshot at effect run time
  }, 1000);
});

Signals and Derived State

CLAUDE.md for Solid Projects

## Solid.js Project

- Solid.js 1.8+, TypeScript, Vite
- Routing: @solidjs/router
- State: createSignal + createStore (no external state library needed)
- Data fetching: createResource (built-in async primitive)
- Forms: @modular-forms/solid

## Reactivity rules
- Access signals with () in JSX — don't destructure objects with signals inside
- Store access: use store.property (tracked), don't destructure ({ property } = store) — loses tracking
- Never read signals outside reactive contexts (components, effects, memos)
- Use createMemo for expensive derived computations (caches result until deps change)
- Use createEffect for side effects that depend on signals (NOT for rendering)

## Component conventions
- Props: use props.x not const { x } = props (destructuring breaks reactivity)
- Children: use props.children
- Refs: use createSignal<HTMLElement | undefined>() — not useRef

Stores for Complex State

// src/stores/cart.ts
import { createStore, produce } from 'solid-js/store';

interface CartStore {
  items: CartItem[];
  coupon: string | null;
  loading: boolean;
}

const [cart, setCart] = createStore<CartStore>({
  items: [],
  coupon: null,
  loading: false,
});

// Derived values — computed from store
export const cartTotal = createMemo(() =>
  cart.items.reduce((sum, item) => sum + item.price * item.quantity, 0)
);

export const itemCount = createMemo(() =>
  cart.items.reduce((count, item) => count + item.quantity, 0)
);

// Mutations using produce (Immer-like, for complex updates)
export function addToCart(product: Product) {
  setCart(produce((state) => {
    const existing = state.items.find(i => i.productId === product.id);
    if (existing) {
      existing.quantity++;
    } else {
      state.items.push({
        productId: product.id,
        name: product.name,
        price: product.price,
        quantity: 1,
        image: product.image,
      });
    }
  }));
}

export function removeFromCart(productId: string) {
  setCart('items', items => items.filter(i => i.productId !== productId));
}

export function updateQuantity(productId: string, quantity: number) {
  if (quantity <= 0) {
    removeFromCart(productId);
    return;
  }
  setCart('items', item => item.productId === productId, 'quantity', quantity);
}

export { cart };
// src/components/Cart.tsx — using the store
import { For, Show } from 'solid-js';
import { cart, cartTotal, itemCount, updateQuantity, removeFromCart } from '../stores/cart';

export function Cart() {
  return (
    <div>
      <h2>Cart ({itemCount()} items)</h2>
      
      <Show when={cart.items.length > 0} fallback={<p>Your cart is empty</p>}>
        <For each={cart.items}>{(item) => (
          // For renders each item — fine-grained updates when items change
          <div>
            <img src={item.image} alt={item.name} />
            <span>{item.name}</span>
            <input
              type="number"
              value={item.quantity}
              min="0"
              onInput={(e) => updateQuantity(item.productId, parseInt(e.currentTarget.value))}
            />
            <span>${(item.price * item.quantity / 100).toFixed(2)}</span>
            <button onClick={() => removeFromCart(item.productId)}>Remove</button>
          </div>
        )}</For>
        
        <div>
          <strong>Total: ${(cartTotal() / 100).toFixed(2)}</strong>
        </div>
      </Show>
    </div>
  );
}

createResource for Data Fetching

// src/pages/UserList.tsx
import { createResource, createSignal, For, Suspense } from 'solid-js';

async function fetchUsers(page: number) {
  const response = await fetch(`/api/users?page=${page}`);
  if (!response.ok) throw new Error('Failed to fetch users');
  return response.json() as Promise<{ users: User[]; total: number }>;
}

export function UserList() {
  const [page, setPage] = createSignal(1);
  
  // createResource: fetches when source signal (page) changes
  // Suspense handles loading state automatically
  const [data, { refetch }] = createResource(page, fetchUsers);
  
  return (
    <div>
      <Suspense fallback={<UsersSkeleton />}>
        {/* JSX inside Suspense suspended while data() is loading */}
        <For each={data()?.users}>{(user) => (
          <UserCard user={user} />
        )}</For>
        
        <div>
          <button
            disabled={page() <= 1}
            onClick={() => setPage(p => p - 1)}
          >
            Previous
          </button>
          <span>Page {page()}</span>
          <button onClick={() => setPage(p => p + 1)}>
            Next
          </button>
        </div>
      </Suspense>
    </div>
  );
}

SolidStart Full-Stack

Create a SolidStart page with server-side data loading
and a server action for form submission.
// src/routes/posts/[id].tsx (SolidStart file-based routing)
import { createAsync, action, useAction } from '@solidjs/router';
import { getPost, updatePost } from '~/lib/posts';

// Server function — runs on server only
const post$ = createAsync(async (params) => {
  'use server';
  return getPost(params.id);
});

// Server action — handles form submission on server
const updateTitle = action(async (formData: FormData) => {
  'use server';
  const id = formData.get('id') as string;
  const title = formData.get('title') as string;
  await updatePost(id, { title });
  return { success: true };
});

export default function PostPage(props) {
  const post = post$(props.params);
  const submit = useAction(updateTitle);
  
  return (
    <Suspense fallback={<p>Loading...</p>}>
      <h1>{post()?.title}</h1>
      
      <form onSubmit={async (e) => {
        e.preventDefault();
        await submit(new FormData(e.currentTarget));
      }}>
        <input name="id" type="hidden" value={post()?.id} />
        <input name="title" value={post()?.title} />
        <button type="submit">Update</button>
      </form>
    </Suspense>
  );
}

For comparing Solid.js with React for specific use cases, see the React frontend guide. For Svelte 5’s Runes which take a similar signals-based approach, see the SvelteKit guide. The Claude Skills 360 bundle includes Solid.js skill sets for fine-grained reactivity patterns. Start with the free tier to convert React components to Solid.

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free