DEV Community

Cover image for SolidJS 2.0: A React Developer's First Look at Signals and Async
Dennis Morello
Dennis Morello

Posted on • Originally published at morello.dev

SolidJS 2.0: A React Developer's First Look at Signals and Async

I've shipped React for the better part of a decade. Hooks, Suspense, Server Components, the whole tour. So when the SolidJS 2.0 beta showed up in my feed with the tagline "The <Suspense> is Over," I rolled my eyes a little and then clicked anyway.

I'm glad I did. Solid has been the framework React people admire from a distance for years, and 2.0 is the release that finally made me install it and poke at the reactivity model instead of just reading about it. This post is my honest first look: what's new, what a React developer will recognize, and where Solid quietly does something React can't.

What is SolidJS 2.0?

SolidJS is a UI library with React-like JSX and a completely different engine underneath. Instead of re-rendering components and diffing a virtual DOM, it uses fine-grained reactivity: your component runs once, and updates flow directly to the exact DOM nodes that depend on a piece of state.

The 2.0 line is currently in beta. The first public build, v2.0.0-beta.0, landed on March 3, 2026, and the team skipped the alpha phase entirely because the milestones planned for it stopped feeling worth their own release. You can try it today:

pnpm add solid-js@next
Enter fullscreen mode Exit fullscreen mode

The headline of the whole release is async. Solid's reactive graph now understands promises natively, and a lot of the API changes fall out of that one decision.

Fine-grained reactivity, from a React brain

Here's the mental model shift, and it's the thing to internalize before anything else makes sense.

In React, state changes re-run your component function. React then builds a new virtual DOM tree and diffs it against the old one to figure out what actually changed. useMemo, useCallback, and dependency arrays exist to keep that re-render machine from doing too much work. The React Compiler automates a lot of that memoization now, but the underlying model is the same: the component re-runs and React diffs the result.

In Solid, the component body is setup code. It runs a single time. When you read a signal in your JSX, Solid records that specific dependency and wires it straight to the DOM. Change the signal later and only that text node or attribute updates. No component re-render, and no dependency array to keep honest.

function Counter() {
  const [count, setCount] = createSignal(0);

  // This whole function runs ONCE. Only the text node updates on click.
  return <button onClick={() => setCount(count() + 1)}>{count()}</button>;
}
Enter fullscreen mode Exit fullscreen mode

The tell for a React dev: count is a function, not a value. You call count() to read it. That call is what subscribes the surrounding computation to changes. Once it clicks, the absence of a dependency array stops feeling like something is missing and starts feeling like a bug class that no longer exists.

First-class async is the real story

This is where 2.0 earns its version bump. In Solid 1.x, async data meant createResource and wrapping things in <Suspense>. In 2.0, a derived computation can just return a promise, and the graph handles suspending and resuming for you.

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

// Solid 2.0: the promise flows through the reactive graph
const data = createMemo(() => fetchUser(userId()));
Enter fullscreen mode Exit fullscreen mode

Suspense gets replaced by <Loading>, and the semantics are the part I actually care about. <Loading> is scoped to initial readiness. It shows a fallback while the subtree can't render anything yet, and then it stays out of your way. When userId() changes and the data refetches, the UI doesn't tear itself down and flash a spinner. It holds the old content until the new content is ready.

To show that a refresh is in flight, you reach for isPending, which reports on pending reactive work without unmounting anything:

const users = createMemo(() => api.listUsers());
const refreshing = () => isPending(() => users());

<>
  <Show when={refreshing()}>
    <RefreshIndicator />
  </Show>
  <Loading fallback={<Spinner />}>
    <UserList users={users()} />
  </Loading>
</>;
Enter fullscreen mode Exit fullscreen mode

React 19 gets you most of the way here: put the update in a transition with useTransition, and Suspense holds the old content on screen instead of flashing its fallback, with isPending reporting the refresh. The difference is that React makes you opt into that at each call site, while Solid bakes the distinction between "we have nothing to show yet" and "we're refreshing what's already on screen" into the primitives themselves.

Mutations have a home now

For a long time React had no blessed way to do writes, so you rolled your own or reached for a data library. React 19 changed that with Actions and the useOptimistic hook, so this is a spot where the two frameworks have converged. Solid 2.0's version is action for mutations, plus createOptimistic and createOptimisticStore for optimistic updates, with refresh to revalidate afterward.

const [messages, setMessages] = createOptimisticStore(() => chatServer.loadMessages(), []);

const sendMessage = action(function* (next) {
  setMessages((m) => {
    m.push(next); // optimistic: show it instantly
  });
  yield chatServer.sendMessage(next); // await the server
  refresh(messages); // reconcile with the source of truth
});
Enter fullscreen mode Exit fullscreen mode

The generator function is doing real work here. Each yield is a point where the action awaits, and the reactive system tracks the optimistic state until the real value lands and reconciles. Solid has been circling this for a while: @solidjs/router already shipped createAsync and action for data loading and mutations. 2.0 takes that thinking and builds async and actions into the core reactive graph, so the patterns aren't router-specific anymore.

Deterministic batching, and a gotcha

One behavioral change will trip you up if you're not ready for it. Writes are batched on a microtask, and reads don't reflect a write until the batch flushes.

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

setCount(1);
count(); // still 0: the update is queued on the microtask
flush(); // apply queued updates synchronously
count(); // now 1
Enter fullscreen mode Exit fullscreen mode

flush() forces the queue to apply right away when you need to read the result immediately, like focusing an input after a state change. Coming from React's synchronous batching it takes a moment to adjust, but the model is predictable, and that matters once async is woven through everything.

The renames a React developer should know

Solid 2.0 is a major version, so it cleans house. Since this is beta, some of these identifiers could still shift before stable, but the direction is set, and the migration guide tracks where things stand today. The changes that matter most coming from React:

  • <Suspense><Loading>, with the initial-readiness semantics above. <SuspenseList> becomes <Reveal>.
  • createEffect is split into a compute phase (what to track) and an apply phase (what to run with the result). Instead of reading signals inside one callback, you pass the two separately:
  // Solid 1.x
  createEffect(() => apiCall(signalB()));

  // Solid 2.0
  createEffect(signalB, apiCall);
  createEffect(
    () => [signalA(), signalB()],
    ([a, b]) => a && apiCall(b),
  );
Enter fullscreen mode Exit fullscreen mode

If you've ever shipped a bug because a value was in a useEffect body but missing from the dependency array, separating the two is a direct answer to that. Ryan Carniato has also said the idiomatic 2.0 code reaches for createEffect far less than 1.x did, since async and derived state now cover cases you used to handle with an effect.

  • onMountonSettled, reflecting the async-aware lifecycle.
  • <Index><For keyed={false}>; in that non-keyed mode the row is passed as an accessor, matching the old <Index> stability model.
  • Store setters are draft-first by default. You mutate a draft instead of threading a path. This is 1.x's produce behavior promoted to the default, so you no longer wrap the callback, with a storePath helper as the opt-in escape hatch for the old path style:
  // 2.0 default: mutate the draft
  setStore((s) => {
    s.todos[id].done = true;
  });
  // Legacy path style, opt-in
  setStore(storePath("todos", id, "done", true));
Enter fullscreen mode Exit fullscreen mode
  • classList is folded into class, which now takes strings, arrays, and objects.
  • use: directives are removed in favor of ref directive factories.
  • Context is simpler. The context is directly usable as the provider, so Context.Provider is gone. A context created without a default is typed as its value instead of T | undefined, and useContext throws ContextNotFoundError when there's no provider above it; a context created with a default still returns that default:
  const Theme = createContext(); // no default
  // 2.0: the context is its own provider
  <Theme value="dark">{/* ... */}</Theme>;
Enter fullscreen mode Exit fullscreen mode

Signals are heading toward the standard

The word "signals" is everywhere now, and it's worth knowing why. There's a TC39 Signals proposal at Stage 1, with design input from the people behind Angular, Vue, Svelte, Preact, Qwik, MobX, and Solid. Ryan Carniato, Solid's creator, has been part of that conversation for years, and his writing on fine-grained reactivity helped push it onto the committee's radar.

Worth being precise here, because it's easy to overstate: Solid 2.0 does not ship the TC39 API as its core. The proposal is synchronous, and Solid 2.0's whole thing is async reactivity, which goes further than what's currently on the table. Solid influences and tracks the standard rather than implementing it. But the fact that the primitive Solid has bet on for years is now a serious standardization effort tells you the industry is drifting toward the model React chose not to adopt. Svelte 5 already rebuilt its reactivity around signals with its runes API, and it's unlikely to be the last framework to make the jump.

Should you drop React? No. But watch this closely.

I'm not migrating my day job to Solid this quarter, and I don't think you should either. It's a beta, the ecosystem is smaller than React's, and API names are still moving. That's the honest read.

But 2.0 is the most interesting frontend release I've looked at in a while, precisely because it isn't chasing React. It takes fine-grained reactivity seriously and follows the idea all the way through to async, and the result is a set of primitives that make the hard parts of React (stale closures, dependency arrays, Suspense re-triggering, figuring out where mutations should live) feel like problems you can just stop having.

If you write React and you've never actually run Solid, this is the release to spend a weekend on. Spin up a project with solid-js@next, build something small with createSignal and <Loading>, and pay attention to how much of your usual React vigilance you can put down.

The beta is live at github.com/solidjs/solid, and the announcement discussion is worth reading in full if any of this piqued your interest.

Top comments (0)