Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 147 additions & 0 deletions .claude/skills/solid-control-flow/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
---
name: solid-control-flow
description: How to use Solid's control-flow components in JSX. Use when writing or reviewing markup that needs conditionals, lists, error handling, or async fallbacks, or that reaches for Show, For, Repeat, Switch, Match, Dynamic, Loading, Errored, Reveal, or Portal. Covers the callback-child and keyed semantics that decide whether content remounts.
---

# Solid control flow

Never use a ternary or `.map()` where one of these fits. They exist so the
runtime can keep DOM across updates instead of rebuilding it.

## Show

Renders children when `when` is truthy, otherwise `fallback`.

```tsx
<Show when={user()} fallback={<SignIn />}>
<Greeting />
</Show>
```

A function child receives the narrowed value, and `keyed` decides its shape
and its remount behavior.

```tsx
// Default. The child gets an accessor and is preserved across truthy values.
<Show when={user()}>{u => <Greeting name={u().name} />}</Show>

// Keyed. The child gets the raw value and remounts when identity changes.
<Show when={user()} keyed>{u => <Greeting name={u.name} />}</Show>
```

The narrowing only holds while the condition is truthy. Reading the accessor
after the block has gone throws. Do not stash it in a timer or an async
continuation.

## For

Renders a list. The child is a callback, not markup.

```tsx
<For each={items()} fallback={<div>No items</div>}>
{(item, index) => <div data-index={index()}>{item.label}</div>}
</For>
```

The `keyed` prop picks which argument is reactive, so it decides what remounts
when the list changes:

- default or `keyed={true}`: `(item, index)` where `item` is the raw row and
`index` is an accessor. Rows remount when the row value changes.
- `keyed={false}`: `(item, index)` where `item` is an accessor and `index` is
a plain number. Rows survive value changes and update in place. This is what
Solid 1.x called `Index`.
- `keyed={item => item.id}`: both arguments are accessors, and identity is the
returned key. Use this for rows that move.

`each` accepts `undefined`, `null`, and `false` as an empty list, so a
not-yet-loaded list renders the fallback without a guard.

## Repeat

A list from a count rather than from data.

```tsx
<Repeat count={10} fallback={<Empty />}>
{index => <Row index={index} />}
</Repeat>
```

`from` shifts the starting index. The child may be static markup instead of a
callback.

## Switch and Match

Mutually exclusive conditions. The first truthy `Match` wins.

```tsx
<Switch fallback={<NotFound />}>
<Match when={state.route === "home"}>
<Home />
</Match>
<Match when={state.route === "settings"}>
<Settings />
</Match>
</Switch>
```

`Match` takes the same function-child and `keyed` semantics as `Show`. Only
`Match` elements may be direct children of `Switch`.

## Loading

The async boundary. Any read that is still pending throws, and the nearest
`Loading` swaps to its fallback until everything settles.

```tsx
<Loading fallback={<Spinner />}>
<Profile />
</Loading>
```

Scope it around the data-dependent slot, not the surrounding shell. Wrapping
the header, nav, and footer in the same boundary as the data means a
revalidation replaces the whole screen with the fallback. Chrome rendered
outside the boundary stays stable while only the data slot flips.

The `on` prop scopes the boundary to transitions caused by specific sources,
so writes elsewhere leave the current content up.

## Errored

Catches uncaught errors in its subtree.

```tsx
<Errored fallback={(err, reset) => <div onClick={reset}>Failed: {String(err())}</div>}>
<Widget />
</Errored>
```

The fallback may be markup or a callback taking the error accessor and a
`reset` function. Errors thrown by the fallback itself reach the parent
boundary.

## Dynamic

An element or component chosen at runtime.

```tsx
<Dynamic component={multiline() ? RichTextEditor : "input"} value={value()} onInput={onInput} />
```

Every other prop is forwarded. Prefer a plain tag when the choice is static,
because a literal tag compiles into the template and a `Dynamic` does not.

## Portal and Reveal

`Portal` renders into a different mount point, for overlays and modals.
`Reveal` with `createRevealOrder` controls the order in which sibling async
content appears, replacing Solid 1.x's `SuspenseList`.

## Compiler note

These components are auto-imported by the compiler when the name is not
otherwise bound, so a bare `<Show>` works without an import. An explicit
`import { Show } from "solid-js"` works too, including under an alias. A local
binding of the same name shadows the built-in and is treated as an ordinary
component.
134 changes: 134 additions & 0 deletions .claude/skills/solid-reactivity/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
---
name: solid-reactivity
description: How to use Solid 2.0's reactive primitives correctly. Use when writing or reviewing code that calls createSignal, createMemo, createEffect, createStore, createProjection, or createOptimistic, or when async data is involved. Covers the signatures that changed from Solid 1.x and the 1.x APIs that no longer exist.
---

# Solid reactivity

Solid 2.0 changed several signatures that look unchanged. Code written from
Solid 1.x habit compiles and then behaves wrong. Check this file before
writing a primitive call.

## createEffect takes two functions

This is the most common mistake. `createEffect` is split into a tracked
compute and an untracked effect.

```ts
// Correct.
createEffect(
() => count(), // compute: tracks, runs in the compute phase
value => console.log(value) // effect: side effects, runs after the flush
);

// Wrong. This is the Solid 1.x shape and throws MISSING_EFFECT_FN in dev.
createEffect(() => console.log(count()));
```

Return a cleanup from the effect function. It runs before the next run and on
disposal.

```ts
createEffect(
() => userId(),
id => {
const controller = new AbortController();
fetch(`/users/${id}`, { signal: controller.signal });
return () => controller.abort();
}
);
```

Pass `{ effect, error }` instead of a plain function to handle errors that
arrive from the compute or from upstream sources, including async rejections.
Errors thrown inside the effect function itself are not routed there. Wrap
those in `try`/`catch` yourself, because an uncaught one reaches the nearest
error boundary and halts the reactive system if there is none.

```ts
createEffect(() => user(), {
effect: value => render(value),
error: err => setMessage(String(err))
});
```

## A function argument makes a signal reactive

`createSignal` has two shapes. With a value it is a plain signal. With a
function it is a writable memo: it recomputes from its sources, and the setter
applies a local override.

```ts
const [count, setCount] = createSignal(0); // plain
const [user, setUser] = createSignal(() => fetchUser(userId())); // writable memo

setUser({ ...user(), name: "Alice" }); // local optimistic edit
```

`createStore` works the same way. With an object it is a store. With a
function it is a projection seeded by the second argument.

```ts
const [state, setState] = createStore({ items: [] });
const [view, setView] = createStore(
draft => {
draft.total = sum(state.items);
},
{ total: 0 }
);
```

## Async is a function that throws while pending

There is no `createResource` and no `createAsync`. Any computation can be
async: pass a function that returns a promise or async iterable, and a read
while it is pending throws. The nearest `<Loading>` catches that and shows its
fallback.

```tsx
const [user] = createSignal(() => fetch(`/users/${id()}`).then(r => r.json()));

<Loading fallback={<Spinner />}>
<Profile name={user().name} />
</Loading>;
```

Do not try to branch on a loading flag inside the computation. Let the read
throw and let the boundary handle it. Use `latest()` to read the previous
settled value instead of suspending, and `isPending()` to check without
triggering.

Give a computation a `loadingValue` to render a placeholder rather than
suspend. Type the placeholder honestly: if it stands in for real data, mark it
in the data, for example with a `skeleton: true` field, rather than letting it
impersonate a settled value.

## Solid 1.x APIs that are gone

Reach for the replacement, not the old name.

- `createResource` and `createAsync`: any computation is async, see above.
- `batch`: use `flush`.
- `createSelector`: use `createProjection`.
- `createComputed` and `createDeferred`: no replacement, restructure.
- `on`: no longer needed, the compute argument of `createEffect` is the
dependency list.
- `onMount`: use `onSettled`.
- `onError` and `catchError`: use `createErrorBoundary` or `<Errored>`.
- `startTransition` and `useTransition`: removed.
- `equalFn`: renamed `isEqual`.
- `getListener`: renamed `getObserver`.
- `unwrap`: renamed `snapshot`.
- `createMutable`, `modifyMutable`, `produce`: `produce` behavior is the
default store setter now.
- `indexArray` and `Index`: `<For>` covers both, see the control-flow skill.
- `observable` and `from`: use async iterators.

The full map with the reasoning is in the `Not Implemented` block at the
bottom of `packages/solid/src/index.ts`.

## Reading without tracking

`untrack(fn)` reads without subscribing. Prefer restructuring so the read is
outside the tracked scope; reach for `untrack` when that is not possible.
`snapshot(store)` returns a plain non-reactive copy of a store.
Loading
Loading