Start here: If you’re migrating an app, read the beta tester guide first: MIGRATION.md
Solid 2.0 simplifies and unifies control-flow APIs by consolidating list rendering into a single For signature (covering the old For/Index split), introducing Repeat for range/count-based rendering, and renaming/reshaping async and error boundaries as Loading and Errored. The goal is fewer “nearly-the-same” APIs, more explicit keying semantics, and control-flow callbacks that are consistent with the 2.0 reactivity model.
- One list primitive: Having both
ForandIndexencourages bikeshedding and accidental misuse. A singleForthat can be keyed or index-based is easier to teach and document. - Ranges without diffing: Rendering “count-based” lists (skeletons, ranges, windowing) shouldn’t require list diffing;
Repeatexpresses this directly. - Async and error UX: Names like Suspense and ErrorBoundary are long and carry baggage.
LoadingandErroredare concise and align better with their actual role in the 2.0 async model.
For takes each, optional fallback, optional keyed, and a children mapping function that receives accessors for both the item and the index.
// Default keyed behavior (identity)
<For each={todos()}>
{(todo, i) => <TodoRow todo={todo()} index={i()} />}
</For>
// Index-style behavior (reuse by index)
<For each={todos()} keyed={false}>
{(todo, i) => <TodoRow todo={todo()} index={i()} />}
</For>
// Custom key
<For each={todos()} keyed={(t) => t.id}>
{(todo) => <TodoRow todo={todo()} />}
</For>
// Fallback
<For each={todos()} fallback={<EmptyState />}>
{(todo) => <TodoRow todo={todo()} />}
</For>Notes:
keyed={false}is the direct replacement forIndex.keyed={(item) => key}is the escape hatch for stable keys without having to pre-normalize lists.
Repeat renders based on count (and optional from), with no list diffing. Unlike For, children receive a plain number (not an accessor) — the index itself is stable and never changes for a given slot.
This is primarily intended for use with stores, where the data at each index manages its own granular updates. The index is just a stable lookup key; reactivity comes from the store reads, not the index changing:
// Store-backed list: index is stable, store handles granular updates
<Repeat count={store.items.length}>
{(i) => <Row name={store.items[i].name} status={store.items[i].status} />}
</Repeat>// Skeletons
<Repeat count={10}>{(i) => <Skeleton key={i} />}</Repeat>
// Windowing / offset
<Repeat count={visibleCount()} from={start()}>
{(i) => <Row index={i} />}
</Repeat>
// Fallback when count is 0
<Repeat count={store.items.length} fallback={<EmptyState />}>
{(i) => <div>{store.items[i].label}</div>}
</Repeat>Show supports element children or function children. Function children receive a narrowed accessor.
<Show when={user()} fallback={<Login />}>
{(u) => <Profile user={u()} />}
</Show>
// Keyed form (treats value identity as the switching condition)
<Show when={user()} keyed>
{(u) => <Profile user={u()} />}
</Show>Switch picks the first matching Match. Match supports element or function children.
<Switch fallback={<NotFound />}>
<Match when={route() === "home"}>
<Home />
</Match>
<Match when={route() === "profile"}>
<Profile />
</Match>
</Switch>Loading is the boundary for async computations. It shows fallback while async values required by its subtree are not ready.
<Loading fallback={<Spinner />}>
<UserProfile id={params.id} />
</Loading>In 2.0’s async model, async values are part of computations (not a separate createResource), so Loading is the user-facing “this subtree may suspend” boundary.
Loading also accepts an on prop to control when the boundary re-shows its fallback during revalidation. See RFC 05 for details.
Errored is the error boundary. It supports a static fallback or a callback form that receives the error and a reset function.
<Errored
fallback={(err, reset) => (
<div>
<p>Something went wrong.</p>
<pre>{String(err)}</pre>
<button onClick={reset}>Retry</button>
</div>
)}
>
<Page />
</Errored>Reveal coordinates the reveal timing of sibling Loading boundaries. It replaces SuspenseList from 1.x.
- Sequential (default): boundaries reveal in DOM order as each resolves.
- Together (
together): all boundaries wait until the group is ready, then reveal at once. - Collapsed (
collapsed, sequential only): only the frontier boundary shows its fallback; later boundaries produce nothing until their turn.
<Reveal>
<Loading fallback={<Skeleton />}><ProfileHeader /></Loading>
<Loading fallback={<Skeleton />}><Posts /></Loading>
</Reveal>
// Together mode — all reveal at once
<Reveal together>
<Loading fallback={<Skeleton />}><ProfileHeader /></Loading>
<Loading fallback={<Skeleton />}><Posts /></Loading>
</Reveal>
// Collapsed mode — only the frontier shows fallback
<Reveal collapsed>
<Loading fallback={<Skeleton />}><ProfileHeader /></Loading>
<Loading fallback={<Skeleton />}><Posts /></Loading>
</Reveal>// 1.x
<SuspenseList revealOrder="forwards">
<Suspense fallback={<Skeleton />}><ProfileHeader /></Suspense>
<Suspense fallback={<Skeleton />}><Posts /></Suspense>
</SuspenseList>
// 2.0
<Reveal>
<Loading fallback={<Skeleton />}><ProfileHeader /></Loading>
<Loading fallback={<Skeleton />}><Posts /></Loading>
</Reveal>// 1.x
<Index each={items()}>
{(item, i) => <Row item={item()} index={i} />}
</Index>
// 2.0
<For each={items()} keyed={false}>
{(item, i) => <Row item={item()} index={i()} />}
</For>// 1.x
<Suspense fallback={<Spinner />}>
<Page />
</Suspense>
// 2.0
<Loading fallback={<Spinner />}>
<Page />
</Loading>// 1.x
<ErrorBoundary fallback={(err, reset) => <Fallback err={err} reset={reset} />}>
<Page />
</ErrorBoundary>
// 2.0
<Errored fallback={(err, reset) => <Fallback err={err} reset={reset} />}>
<Page />
</Errored>| Removed | Replacement |
|---|---|
Index |
For keyed={false} |
Suspense |
Loading |
SuspenseList |
Reveal |
ErrorBoundary |
Errored |
- Keeping both
ForandIndex: rejected in favor of one API with explicit keying. - Adding a separate “range” mode to
For: rejected in favor of a dedicatedRepeatthat makes “no diffing” obvious.