# SolidJS 2.0 Async Data: A Deep Dive for React Devs

Published: July 16, 2026
Tags: solidjs, react, javascript, webdev

How SolidJS 2.0 handles async data: promises in the reactive graph, the Loading boundary, isPending, and generator actions, explained for React developers.

In [my first look at SolidJS 2.0](/blog/solidjs-2-react-developers-first-look) I said first-class async was the real story, then barely opened the box. This post opens it.

Here's the short version. In Solid 2.0 a promise is just another value the reactive graph knows how to wait for. A computation can return one, anything that reads it suspends until it resolves, and a `<Loading>` boundary shows a fallback only until the first real value lands. There's no `createResource`, no manual `await` in your components, and no re-render. If you write React, this is the part that behaves least like what you're used to, so it's worth slowing down on.

Everything below is from the `2.0.0-beta.18` line (`npm install solid-js@next`). It's a beta, and some of these names will still move before stable, so I've flagged the parts that are rougher.

## How does async data fetching work in Solid 2.0?

A derived computation can return a promise, and the reactive graph treats the pending state as "not ready" until it settles. `createResource` is gone; a plain `createMemo` that returns a promise does the job, as the [async RFC](https://github.com/solidjs/solid/blob/next/documentation/solid-2.0/05-async-data.md) puts it: async is "a first-class capability of computations."

```jsx
// Solid 1.x
const [user] = createResource(userId, fetchUser);

// Solid 2.0: the memo returns a promise, the graph waits on it
const user = createMemo(() => fetchUser(userId()));
```

Read `user()` anywhere and you get the resolved value. While the promise is in flight the read throws a `NotReadyError` internally, which propagates up the graph to the nearest boundary rather than crashing. You never catch it yourself; it's the mechanism that lets `<Loading>` and error boundaries work without a dedicated suspense primitive.

When `userId()` changes, the memo re-runs, returns a new promise, and the graph waits again. Same accessor, no dependency array, no effect wired up to refetch.

If you need the value imperatively outside a reactive scope (a route loader, an event handler), `resolve` hands you a real promise that settles once the expression is ready:

```js
const currentUser = await resolve(() => user());
```

One correction to a thing you'll see repeated: **there is no `createAsync` in core 2.0.** `createAsync` is a `@solidjs/router` API, and its 2.0 shape isn't settled in the sources I could find, so don't reach for it as the core data primitive. The core primitive is the async `createMemo` above.

## What replaced `<Suspense>`? The `<Loading>` boundary

`<Loading>` is the direct replacement for 1.x's `<Suspense>`, and the semantics are the reason the release was titled ["The `<Suspense>` is Over."](https://github.com/solidjs/solid/releases/tag/v2.0.0-beta.0) `<Loading>` covers **initial readiness only**. It shows its fallback while the subtree has nothing to render yet, and once real content is on screen it gets out of the way.

```jsx
<Loading fallback={<Spinner />}>
  <Profile user={user()} />
</Loading>
```

This is the behavior React's Suspense makes you fight for. In React, when a value a Suspense boundary depends on changes, the boundary can rip the rendered content down and flash the fallback again unless you wrap the update in [`useTransition`](https://react.dev/reference/react/useTransition). Solid inverts the default: after the first paint, a refetch holds the old content on screen. It won't kick you back to the spinner.

When you do want the fallback to reappear on a specific change (say the whole record is being swapped, not refreshed), the new `on` prop opts into that:

```jsx
<Loading on={userId()} fallback={<Spinner />}>
  <Profile user={user()} />
</Loading>
```

Now a change to `userId()` while data is pending re-shows the fallback; other pending work doesn't.

## How do you coordinate several boundaries? `<Reveal>`

`<Reveal>` replaces 1.x's `<SuspenseList>` and controls the order in which sibling `<Loading>` boundaries reveal their content. It takes an `order` prop of `"sequential"` (the default), `"together"`, or `"natural"`:

- `sequential` reveals boundaries in DOM order; later ones stay on their fallbacks until every earlier one has resolved.
- `together` holds every fallback until the whole group is ready, then reveals at once.
- `natural` lets each boundary reveal as soon as its own data lands.

There's also a `collapsed` boolean, consulted only under `order="sequential"`, that renders a single frontier fallback instead of one per boundary. The [control-flow RFC](https://github.com/solidjs/solid/blob/next/documentation/solid-2.0/03-control-flow.md) is the only source documenting the fine-grained nesting rules here, so treat the exact `<Reveal>` semantics as provisional until stable.

## How do you show a refresh without a spinner? `isPending`

`isPending` reports that a change to a specific read is in flight, so you can show a subtle indicator while the stale content stays put. It takes a thunk, and it actually performs the read, so where you place it matters.

```jsx
const refreshing = () => isPending(() => user());

<Loading fallback={<Spinner />}>
  <Show when={refreshing()}>
    <RefreshBar />
  </Show>
  <Profile user={user()} />
</Loading>;
```

First load hits `<Loading>` and shows the spinner. A later refetch keeps `<Profile>` visible and flips `refreshing()` to `true`, so you render a thin bar instead of tearing the page down. Stale-while-revalidate comes built into the primitive, so you don't assemble it yourself.

The gotcha that will catch you: **a bare `refresh()` reads as not pending.** Re-asking the same question (a poll, a manual refresh, a confirming refetch after a mutation) is treated as silent by design. If you want that reload to register as pending, you declare it with `affects`:

```js
// Silent: isPending stays false, no refresh UI
refresh(user);

// Declared: now the refresh reads as pending
affects(user);
refresh(user);
```

`affects` is one of the newer, rougher corners of the API (it lives in a single RFC family and is likely to move), but the underlying rule is worth knowing now: pending is about a *changed input*, not about work happening. There's also a `latest(fn)` helper that peeks at the in-flight value during a transition and falls back to stale if the next value isn't ready, for when you want to show the incoming id before its data lands.

## Where do mutations live now? `action` and optimistic updates

Solid 2.0 gives writes a home in core with `action`, and the shape is unusual: `action` wraps a **generator**. Each `yield` is a point where the action awaits, which lets the reactive system track an optimistic value until the real one lands.

```js
const [todos, setTodos] = createOptimisticStore(() => api.getTodos(), []);

const addTodo = action(function* (todo) {
  setTodos((t) => {
    t.push(todo); // optimistic: show it immediately
  });
  yield api.addTodo(todo); // await the server
  refresh(todos); // reconcile with the source of truth
});
```

`createOptimistic` has the same surface as `createSignal`, but its writes are optimistic: they can be overridden during a transition and revert when the transition completes. `createOptimisticStore` is the store version. The RFCs document it as `createOptimisticStore(fnOrValue, seed, options?)` while the [migration guide](https://github.com/solidjs/solid/blob/next/documentation/solid-2.0/MIGRATION.md) shows a single-object form, so the exact arity is still settling; the two-argument derived form above is the one that's fully specified.

`refresh(x)` asks a derived signal or store to recompute. It's imperative revalidation, not a piece of UI state, which is why the `affects` dance above exists for the cases where you want a refresh to be visible. An async-generator form of `action` also works, where a bare `yield;` resumes the action in the same transition context after an `await`.

If you've used React 19's [Actions and `useOptimistic`](https://react.dev/reference/react/useOptimistic), the intent is familiar. The difference is that Solid folds the optimistic write, the server call, and the revalidation into one generator instead of splitting them across a hook and an action.

## Why is batching deterministic now?

Solid 2.0 batches updates on a microtask by default, so a setter queues the write and reads don't reflect it until the batch flushes. `batch()` is removed; `flush()` is how you apply pending updates synchronously when you need the result right away.

```js
const [count, setCount] = createSignal(0);

setCount(1);
count(); // still 0: queued on the microtask
flush();
count(); // 1
```

I mentioned this in the first post as a gotcha; the reason it matters here is that it's what makes async reliable. Running the tracking (compute) half of every effect before any side-effecting half gives the graph a complete dependency picture before anything runs, which is exactly what `<Loading>` and error boundaries need to decide what's ready. `flush(fn)` also takes a callback and drains the writes inside it before returning, preserving the return value. Coming from React's synchronous batching it's an adjustment, but the model is predictable once async is threaded through everything.

## What changed for transitions? They're built in

This one's aimed squarely at React developers: **`startTransition` and `useTransition` are gone.** Solid 2.0 treats transitions as a core scheduling concept, and multiple can be in flight at once. Pending UI is expressed through `<Loading>` and `isPending` rather than by wrapping updates. You don't opt in per call site; the framework does the bookkeeping.

Here's the async surface mapped against React 19:

| Task | React 19 | Solid 2.0 |
| --- | --- | --- |
| Fetch data | `use()` + Suspense, or a data library | `createMemo` returning a promise |
| Loading UI | `<Suspense fallback>` (can re-trigger) | `<Loading>` (initial readiness only) |
| Refresh without a flash | `useTransition` + `isPending`, opt-in per call | `isPending(fn)`, observe any read |
| Optimistic update | `useOptimistic` | `createOptimistic` / `createOptimisticStore` |
| Mutations | Actions / form actions | `action(function*)` + `refresh` |
| Batching | automatic sync batching | microtask batching + `flush()` |

The pattern across that table: React exposes each capability as a hook you opt into, while Solid pushes the distinction between "nothing to show" and "refreshing what's shown" down into the primitives, so you get the good behavior by default. That reactivity model is the same shift toward [signals happening across the whole ecosystem](/blog/five-things-you-might-not-know-about-javascript).

## Should you build on this yet?

Not in production. It's `beta.18`, the async core (async `createMemo`, `<Loading>`, microtask batching with `flush`, generator `action`s) is stable enough to learn on, but the edges (`affects`, `createOptimisticStore`'s arity, `<Reveal>`'s nesting rules) are still moving. The announcement is worth reading in full in the [beta.0 discussion](https://github.com/solidjs/solid/discussions/2596).

What I'd actually do, and what I'm doing: build a small data-heavy screen with `solid-js@next`, wire up a `createMemo` that fetches, drop a `<Loading>` around it, and trigger a refetch. Watch the old content stay on screen while the new content loads. That single behavior is the clearest argument for why fine-grained reactivity was worth following all the way to async.
