SolidJS skill for Codex
Run components once, update only what changed. Codex writes Solid components with the run-once model in mind: props are read through the props object instead of destructured, derived values become memos or plain functions, nested data lives in stores updated with produce or reconcile, and lists use For or Index deliberately. In SolidStart it preloads queries per route, mutates through actions that work without JavaScript, and validates every use server function.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- solidjs Installs to ~/.agents/skills/solidjs/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install solidjs The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project solidjs Installs to .agents/skills/solidjs/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/solidjs.zipunzip -o solidjs.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) solidjs PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $solidjs, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Writes SolidJS 1.x components and SolidStart apps with fine-grained reactivity - createSignal, createMemo, createEffect, createResource, stores with produce and reconcile, control-flow components (Show, For, Index, Switch), splitProps and mergeProps, plus SolidStart file routes, query and createAsync, actions with useSubmission, and "use server" functions. Use when building Solid components, porting React code to Solid, fixing props or signals that stop updating, or adding data loading and mutations to a SolidStart app.
Use it when
- Writing or reviewing SolidJS components
- Porting React components or hooks to Solid
- Values in the UI stop updating after a refactor
- Adding data loading, actions or API routes to SolidStart
Use something else when
- The project is React and only borrowing the idea of signals: use react-performance
How to check the result
tsc passes, component tests show the same DOM node updating when a signal or prop changes, and SolidStart forms submit correctly with JavaScript disabled.
The mistake it prevents
Destructuring props or store fields in the component body, which reads them once and freezes the UI at the initial value.
Done well means: components run once and only the exact DOM nodes that depend on a signal update; props are never destructured; derived values are memos or plain functions, not effects writing signals; and in SolidStart every server function validates input and checks auth because it is a public endpoint.
Inspect first
- Read
AGENTS.mdandpackage.json:solid-js(this skill targets 1.9.x; Solid 2.0 changes several APIs, so check the major first),@solidjs/router,@solidjs/start. - SolidStart 1.x has
app.config.tsusingdefineConfigfrom@solidjs/start/configandsrc/app.tsxwith<FileRoutes />. Read both before adding routes. - Check
tsconfig.jsonhas"jsx": "preserve"and"jsxImportSource": "solid-js".
The mental model
A Solid component function runs once. JSX expressions that read signals become subscriptions. Anything that reads a signal outside JSX, a memo, an effect or a function called from those, reads it once and never again.
import { createSignal, createMemo, createEffect, onCleanup, Show, For } from "solid-js";
export function TodoList(props: { initial: Todo[]; onDone?: (id: string) => void }) {
const [todos, setTodos] = createSignal(props.initial);
const [filter, setFilter] = createSignal<"all" | "open">("all");
const visible = createMemo(() =>
filter() === "all" ? todos() : todos().filter((t) => !t.done),
);
const openCount = () => todos().filter((t) => !t.done).length; // cheap: plain derived fn
createEffect(() => {
document.title = `${openCount()} open`;
});
const timer = setInterval(() => console.debug("tick"), 60_000);
onCleanup(() => clearInterval(timer));
return (
<>
<button onClick={() => setFilter(filter() === "all" ? "open" : "all")}>{filter()}</button>
<Show when={visible().length > 0} fallback={<p>Nothing to do</p>}>
<ul>
<For each={visible()}>
{(todo) => (
<li classList={{ done: todo.done }} onClick={() => props.onDone?.(todo.id)}>
{todo.title}
</li>
)}
</For>
</ul>
</Show>
</>
);
}
Decisions
| Need | Use | Not |
|---|---|---|
| Single value | createSignal | |
| Nested object or list edited in place | createStore + setStore(path, value) or produce | signal of a big object replaced on every edit |
| Replace store data from the server, keep identity | setStore(reconcile(newData)) | assigning a fresh object (re-creates every row) |
| Derived and expensive or read in many places | createMemo | effect that sets a signal |
| Derived and cheap | plain function () => a() + b() | |
| Async data in plain Solid | createResource(source, fetcher) | fetch in an effect |
| Async data in SolidStart / Router | query + createAsync | createResource |
| List of objects keyed by identity | <For> | .map() in JSX (recreates nodes) |
| List of primitives or fixed positions | <Index> | |
| Conditional | <Show> / <Switch><Match> | ternaries over large subtrees |
| Side effect after render (DOM, logging, sync to storage) | createEffect | |
| Read without subscribing | untrack(() => x()) |
Props: the rule that bites everyone
// Wrong: destructuring reads once, never updates
function Badge({ count }: { count: number }) { return <span>{count}</span>; }
// Right
function Badge(props: { count: number }) { return <span>{props.count}</span>; }
// Defaults and forwarding
function Button(props: ButtonProps) {
const merged = mergeProps({ variant: "primary" as const }, props);
const [local, rest] = splitProps(merged, ["variant", "children"]);
return <button class={`btn btn-${local.variant}`} {...rest}>{local.children}</button>;
}
props.xis a getter. Pass it on asprops.x, or wrap in a function to store it:const count = () => props.count.- Same for stores:
const { user } = storereads once. Accessstore.user.namein JSX. childrenresolution: use thechildren()helper when you need to inspect or reuse children; callingprops.childrentwice creates the nodes twice.
Porting from React
| React | Solid |
|---|---|
useState | createSignal (call the getter: count()) |
useMemo, useCallback | createMemo; callbacks need no memoization |
useEffect(fn, deps) | createEffect (deps tracked automatically) or on(dep, fn) for explicit deps |
useRef for DOM | let el!: HTMLDivElement; <div ref={el} /> |
className | class, plus classList={{ active: on() }} |
onChange on inputs | onInput for per-keystroke updates |
key in map | <For> tracks by reference; no key prop |
| Context | createContext + useContext, same shape |
Delete dependency arrays and memo() wrappers rather than translating them.
SolidStart data and mutations
// src/routes/projects/[id].tsx
import { query, createAsync, action, useSubmission, reload, type RouteDefinition } from "@solidjs/router";
import { Show } from "solid-js";
const getProject = query(async (id: string) => {
"use server";
const user = await requireUser(); // reads session via getRequestEvent()
return db.project.find(id, user.id);
}, "project");
const renameProject = action(async (formData: FormData) => {
"use server";
const user = await requireUser();
const id = String(formData.get("id"));
const name = String(formData.get("name") ?? "").trim();
if (!name || name.length > 80) return { error: "Name must be 1-80 characters" };
await db.project.rename(id, user.id, name);
return reload({ revalidate: getProject.keyFor(id) }); // refetch only this query
}, "renameProject");
export const route = {
preload: ({ params }) => getProject(params.id),
} satisfies RouteDefinition;
export default function ProjectPage(props: { params: { id: string } }) {
const project = createAsync(() => getProject(props.params.id));
const submission = useSubmission(renameProject);
return (
<Show when={project()} fallback={<p>Loading...</p>}>
{(p) => (
<form action={renameProject} method="post">
<input type="hidden" name="id" value={p().id} />
<input name="name" value={p().name} />
<button disabled={submission.pending}>Save</button>
<Show when={submission.result?.error}>{(e) => <p role="alert">{e()}</p>}</Show>
</form>
)}
</Show>
);
}
querydedupes and caches by key during a navigation;preloadstarts the fetch before the component renders, avoiding waterfalls."use server"functions become RPC endpoints. Validate every argument and check auth inside, never only in the page.- Actions submitted through
<form action={...} method="post">work without JS. - After a mutation, actions revalidate all queries by default; return
reload({ revalidate: key })orredirect(url, { revalidate: [...] })to narrow it. - API routes: export
GET/POSTfunctions from a route file; they receive anAPIEventand return aResponse. - Browser-only components:
clientOnly(() => import("./Map"))from@solidjs/start.
Gotchas
- Early returns in components (
if (!props.user) return null) run once and never re-evaluate. Use<Show>. - Effects run after render and not on the server. Data loading never belongs in one.
- Async work inside an effect loses tracking after the first
await. <For>passes the item as a value and the index as a signal;<Index>does the opposite.- Spreading props onto DOM elements with
{...props}keeps reactivity;Object.assignor object spread into a new object in JS does not.
Verification
npx tsc --noEmit
npx vitest run # with @solidjs/testing-library and vite-plugin-solid
npm run build && npm run start # SolidStart production server
- In a component test, change a signal or prop and assert the DOM updated without
re-mounting (
expect(screen.getByText(...))on the same element). - Submit every SolidStart form with JavaScript disabled.
- Call a
"use server"function’s endpoint without a session and confirm it rejects.
Output
Report components and routes changed, which values are signals, stores or memos and why, queries and their keys, actions and what they revalidate, and the auth check inside each server function.
What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "SolidJS"
short_description: "Run components once, update only what changed"
default_prompt: "$solidjs Port this React dashboard component to SolidJS" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.