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.

  • Frontend
  • $solidjs
  • 205 lines
  • 2 files
  • ~2,100 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- solidjs

Installs to ~/.agents/skills/solidjs/ for every project on this machine.

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

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.

~/.agents/skills/solidjs/SKILL.md

--- name: solidjs description: Writes SolidJS 1.x components and SolidStart apps with fine-grained reactivity - createSignal, createMemo, createEffect, createResource, stores with… license: MIT ---

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

  1. Read AGENTS.md and package.json: solid-js (this skill targets 1.9.x; Solid 2.0 changes several APIs, so check the major first), @solidjs/router, @solidjs/start.
  2. SolidStart 1.x has app.config.ts using defineConfig from @solidjs/start/config and src/app.tsx with <FileRoutes />. Read both before adding routes.
  3. Check tsconfig.json has "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

NeedUseNot
Single valuecreateSignal
Nested object or list edited in placecreateStore + setStore(path, value) or producesignal of a big object replaced on every edit
Replace store data from the server, keep identitysetStore(reconcile(newData))assigning a fresh object (re-creates every row)
Derived and expensive or read in many placescreateMemoeffect that sets a signal
Derived and cheapplain function () => a() + b()
Async data in plain SolidcreateResource(source, fetcher)fetch in an effect
Async data in SolidStart / Routerquery + createAsynccreateResource
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 subscribinguntrack(() => 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.x is a getter. Pass it on as props.x, or wrap in a function to store it: const count = () => props.count.
  • Same for stores: const { user } = store reads once. Access store.user.name in JSX.
  • children resolution: use the children() helper when you need to inspect or reuse children; calling props.children twice creates the nodes twice.

Porting from React

ReactSolid
useStatecreateSignal (call the getter: count())
useMemo, useCallbackcreateMemo; callbacks need no memoization
useEffect(fn, deps)createEffect (deps tracked automatically) or on(dep, fn) for explicit deps
useRef for DOMlet el!: HTMLDivElement; <div ref={el} />
classNameclass, plus classList={{ active: on() }}
onChange on inputsonInput for per-keystroke updates
key in map<For> tracks by reference; no key prop
ContextcreateContext + 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>
  );
}
  • query dedupes and caches by key during a navigation; preload starts 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 }) or redirect(url, { revalidate: [...] }) to narrow it.
  • API routes: export GET/POST functions from a route file; they receive an APIEvent and return a Response.
  • 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.assign or 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.yamlgenerated from this page

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.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem