executing-plans
executing-plans
Use when executing an implementation plan in the current session as the implementer yourself — your human partner chose inline execution, or no subagent tool is available
solid-jsSolidJS — fine-grained reactive UI library without a virtual DOM. Use when building high-performance UIs, choosing a React alternative with better reactivity primitives, or using SolidStart for full-stack apps with SSR and server functions. Covers signals, effects, memos, stores, and migration patterns from React.
Independent GitHub snapshot · Automated classification · Original documentation governs
SolidJS is a declarative UI library that compiles JSX to real DOM operations — no virtual DOM, no diffing. Reactivity is fine-grained: only the exact DOM nodes that depend on changed data update. The result is near-native performance with a React-like authoring experience.
# Official scaffolder — prompts for template, TypeScript and SolidStart
npm create solid@latest
# Same thing without prompts: a client-rendered Solid + Vite app
npm create solid@latest price-table -- --vanilla --ts -t basic
# Or the Vite template
npm create vite@latest price-table -- --template solid-ts
cd price-table && npm install && npm run dev
import { createSignal } from "solid-js";
// Signals are getter/setter pairs
const [count, setCount] = createSignal(0);
// Read: call as function
console.log(count()); // 0
// Write: call setter
setCount(1);
setCount((prev) => prev + 1); // Updater function
import { createSignal, createEffect, onCleanup } from "solid-js";
const [name, setName] = createSignal("Alice");
// First run happens after the component has rendered, then again whenever name() changes
createEffect(() => {
document.title = `Hello, ${name()}!`;
});
// Cleanup goes through onCleanup; a returned value is passed to the next run, not called
createEffect(() => {
const id = setInterval(() => console.log(name()), 1000);
onCleanup(() => clearInterval(id)); // Runs before the next run and on dispose
});
import { createSignal, createMemo } from "solid-js";
const [price, setPrice] = createSignal(100);
const [quantity, setQuantity] = createSignal(3);
// Computed value — only recalculates when dependencies change
const total = createMemo(() => price() * quantity()); // 300
setPrice(120); // total() is now 360
import { Show, For, Switch, Match, Index } from "solid-js";
// Show — conditional rendering
<Show when={isLoggedIn()} fallback={<LoginForm />}>
<Dashboard />
</Show>
// For — list rendering (efficient keyed updates)
<For each={items()}>
{(item, index) => <div>{index()} - {item.name}</div>}
</For>
// Index — list rendering when items change in place (not reorder)
<Index each={items()}>
{(item, index) => <div>{index} - {item().name}</div>}
</Index>
// Switch/Match — multi-branch conditional
<Switch fallback={<p>Unknown status</p>}>
<Match when={status() === "loading"}><Spinner /></Match>
<Match when={status() === "error"}><ErrorView /></Match>
<Match when={status() === "success"}><DataView /></Match>
</Switch>
import { createStore, produce } from "solid-js/store";
const [state, setState] = createStore({
users: [{ id: 1, name: "Alice", active: true }],
loading: false,
});
// Update nested properties
setState("loading", true);
setState("users", 0, "active", false);
// Immer-like mutations with produce
setState(
produce((s) => {
s.users.push({ id: 4, name: "Diana", active: true });
s.loading = false;
})
);
import { createSignal, createResource, ErrorBoundary, Show, Suspense } from "solid-js";
async function fetchUser(id: number) {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error("User not found");
return res.json();
}
function UserProfile() {
const [userId, setUserId] = createSignal(1);
// Refetches automatically when userId() changes
const [user, { refetch, mutate }] = createResource(userId, fetchUser);
// A rejected fetcher surfaces in the nearest ErrorBoundary and as user.error; user.loading is also available
return (
<ErrorBoundary fallback={(err) => <p>Failed: {err.message}</p>}>
<Suspense fallback={<p>Loading...</p>}>
<Show when={user()}>{(u) => <h1>{u().name}</h1>}</Show>
</Suspense>
</ErrorBoundary>
);
}
SolidStart adds file-based routing, SSR, and server functions. SolidStart 2 (August 2026) builds with Vite and Nitro 3 instead of Vinxi and requires Node.js 24 or newer:
npm create solid@latest task-board -- --solidstart --v2 --ts -t basic
cd task-board && npm install && npm run dev
Routes are files: src/routes/index.tsx → /, src/routes/users/[id].tsx → /users/:id, with the shell in src/app.tsx. Configuration lives in vite.config.ts (plugins: [solidStart(), nitro()], imported from @solidjs/start/config and nitro/vite).
Data loading and mutations come from @solidjs/router: wrap reads in query, read them with createAsync, and wrap writes in action (Example 2). Upgrading a v1 app: replace the vinxi scripts with vite dev / vite build, move app.config.ts settings into solidStart() in vite.config.ts, and import HTTP helpers from @solidjs/start/http instead of vinxi/http.
import { createContext, useContext, type ParentComponent } from "solid-js";
import { createStore } from "solid-js/store";
const AuthContext = createContext<{ user: () => User | null; login: (u: User) => void }>();
export const AuthProvider: ParentComponent = (props) => {
const [state, setState] = createStore<{ user: User | null }>({ user: null });
const value = { user: () => state.user, login: (u: User) => setState("user", u) };
return <AuthContext.Provider value={value}>{props.children}</AuthContext.Provider>;
};
export const useAuth = () => useContext(AuthContext)!;
| React | SolidJS |
|---|---|
useState(0) | createSignal(0) |
useEffect(() => {}, [dep]) | createEffect(() => { dep(); }) — dependencies are tracked automatically |
effect cleanup return () => … | onCleanup(() => …) inside the effect |
useMemo(() => calc, [dep]) | createMemo(() => calc()) |
useReducer | createStore |
useContext | useContext (same API) |
React.memo | Not needed — no re-renders |
key prop in lists | Use <For> instead of map() |
useRef | let el!: HTMLDivElement with <div ref={el}> |
User request: "Port our React cart to Solid — it keeps the lines in useState, the subtotal in useMemo, and a useEffect sets the page title."
import { createEffect, createMemo, For } from "solid-js";
import { createStore } from "solid-js/store";
type Line = { sku: string; title: string; price: number; qty: number };
export function Cart(props: { lines: Line[]; taxRate: number }) {
const [lines, setLines] = createStore(props.lines);
const subtotal = createMemo(() => lines.reduce((sum, l) => sum + l.price * l.qty, 0));
const total = () => subtotal() * (1 + props.taxRate);
createEffect(() => {
document.title = `Cart (${lines.length}) — ${total().toFixed(2)}`;
});
return (
<section>
<For each={lines} fallback={<p>Your cart is empty.</p>}>
{(line, i) => (
<label>
{line.title}
<input type="number" min="0" value={line.qty}
onInput={(e) => setLines(i(), "qty", e.currentTarget.valueAsNumber || 0)} />
</label>
)}
</For>
<p>Total: ${total().toFixed(2)}</p>
</section>
);
}
Result: Cart runs once. Changing a quantity updates that line's qty, the total text node and the document title — nothing else is re-created, and no dependency arrays are needed.
User request: "Add a /tasks page to the SolidStart app that lists tasks from the server and has a form to add one."
// src/lib/tasks.ts
import { action, query, redirect } from "@solidjs/router";
type Task = { id: number; title: string; done: boolean };
const tasks: Task[] = [{ id: 1, title: "Write release notes", done: false }]; // stand-in for a database
export const getTasks = query(async () => {
"use server"; // inside the function, not at the top of the file, when wrapped in query/action
return tasks;
}, "tasks");
export const addTask = action(async (formData: FormData) => {
"use server";
const title = String(formData.get("title") ?? "").trim();
if (!title) throw new Error("Title is required");
tasks.push({ id: tasks.length + 1, title, done: false });
throw redirect("/tasks");
}, "addTask");
// src/routes/tasks.tsx
import { For, Show } from "solid-js";
import { createAsync, useSubmission, type RouteDefinition } from "@solidjs/router";
import { addTask, getTasks } from "~/lib/tasks";
export const route = { preload: () => getTasks() } satisfies RouteDefinition;
export default function TasksPage() {
const tasks = createAsync(() => getTasks());
const adding = useSubmission(addTask);
return (
<main>
<ul><For each={tasks()}>{(task) => <li>{task.title}</li>}</For></ul>
<form action={addTask} method="post">
<input name="title" required />
<button disabled={adding.pending}>Add task</button>
</form>
<Show when={adding.error}>{(err) => <p>{err().message}</p>}</Show>
</main>
);
}
Result: /tasks is server-rendered with the Write release notes list item already in the HTML (the <li> carries a data-hk hydration attribute). Submitting the form posts to a generated /_server?id=… endpoint, answers 302 with location: /tasks, and the list re-renders with the new task; it also works before JavaScript has loaded.
const { x } = props reads the value once and breaks reactivity. Use props.x, splitProps, or createMemo.<For> instead of Array.map in JSX for efficient list rendering.createMemo is cached — prefer it over a createEffect that writes to another signal.query."use server" code runs only on the server, but each function becomes a public HTTP endpoint — check the session and validate arguments inside it.solid-js@next) and is not a drop-in upgrade: DOM APIs move to @solidjs/web, store helpers move into solid-js, createEffect splits into compute and apply functions, Suspense/ErrorBoundary become Loading/Errored, Index becomes <For keyed={false}>, and createResource is replaced by async memos. Everything above targets 1.9; read the 2.0 migration guide in the next branch before adopting it.