Skip to content

Papercuts ReactiveScript Solves

Posted on:November 14, 2026

As some of you have seen there is a new project from Ryan Carniato that’s been released and it’s called ReactiveScript.

Initially I was a bit skeptical of writing UIs using a new language when we already have TypeScript. Over time I’ve slowly become more intrigued as it solves some of the papercuts people have when authoring Solid.

It is something that allows you to express the entirety of Solid’s primitives with very terse syntax that is correct by construction. Agents seem to love writing it as shown by some early benchmarks.

Below is a list of some things I’ve found it improves.

Plain variable access

Might as well start with the obvious one. People always want to just use plain variables over the function calls so now you can.

export default component App() {
  source count := 0
  double = count * 2   // no createMemo, no () =>

  <button type="button" onClick={() => { count := count + 1 }}>
    Clicked {count} times (doubled {double})
  </button>
}

Notice {count} instead of {count()}

Explicit writes without the tuple

Some people have been turned off by the tuple.

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

This is recommended because it keeps the writes explicit and you can see them in your local scope. ReactiveScript provides the same guarantees.

Only the block that declares a source can write it, with :=. Pass count to a child and it is read-only there. To let the child change it, pass a handler:

component Counter({ count, onInc }: { count: number; onInc: () => void }) {
  <button onClick={() => { onInc() }}>{count}</button>
}

export default component App() {
  source count := 0
  <Counter count={count} onInc={() => { count := count + 1 }} />
}

Imports are read-only too. A module that exports a source exports actions to change it, so import { cart, addToCart } follows the same rule as props.

Destructuring props keeps them live

Another pain point, especially for people coming from React, is that you can’t destructure props without losing reactivity. In ReactiveScript it preserves it for you, defaults and rest included.

component Badge({ label, tone = "info", ...rest }) {
  <span class={tone} {...rest}>{label}</span>
}

Actions don’t require generators

So remember how in Solid you have to use a generator with actions? Or in React you need a nested startTransition. It is needed no more!

// Solid 2
const save = action(function* () {
  yield saveItem();
});

// ReactiveScript
action save() {
  try {
    await saveItem()
    status := "saved"
  } catch (e) {
    status := "failed"
  }
}

Note the try. An action has to handle its own failures, or it doesn’t compile, so unhandled rejections are one less thing to chase down.

No loss of tracking after await

Another common gotcha is losing tracking after an await. This is no longer a concern with ReactiveScript. Reads after an await are tracked like any other read, because ReactiveScript collects dependencies at compile time.

import pure { fetchUser } from "./api"

source id := 1
source lang := "en"

// lang is tracked even though it’s read after the await
greeting = `${(await fetchUser(id)).name} (${lang})`

Automatically chooses createSignal vs createStore

Lastly, another common complaint is needing to choose between createSignal and createStore. Svelte and React don’t have this problem because state is just one type of thing. Solid has maintained that it’s a valuable split for performance reasons mainly.

The DX of the authoring experience suffers from having one additional choice to make. ReactiveScript can now make that choice for you!

// createSignal
source count := 0

// createStore
source user := {
  'first': 'Brenley',
  'last': 'Dueck'
}

<button onClick={() => { user.first := "Lance" }}>rename</button>

The rename writes one path, so only readers of user.first update. That’s the store benefit, without having to ask for it.

Mistakes become compile errors

Many of these come down to one idea: mistakes that Solid lets through silently, or only warns about at runtime, are refused by the compiler.

Here’s one that catches people in Solid 2. Writes are batched, so a signal you just set still reads its old value until the batch flushes:

const [items, setItems] = createSignal(0);
const [total, setTotal] = createSignal(0);

const add = () => {
  setItems(items() + 1);
  setTotal(items() * 5); // still reads the old items()
};

There’s no warning. The total is just one item behind. Write the same thing in ReactiveScript:

<button onClick={() => {
  items := items + 1
  total := items * 5
}}>{items} items, ${total}</button>

and it doesn’t compile:

error STALE_OWN_READ [AC-1d] items is read after this change wrote it: the read sees the committed value, not the write (W-4) — keep the value you wrote in a local (`let v = …; items := v`) and read that, or write functionally (`items := v => …`, W-6)

The error tells you the fix:

<button onClick={() => {
  let next = items + 1
  items := next
  total := next * 5
}}>{items} items, ${total}</button>

Every error names the rule it enforces and usually the fix. That helps people, and it helps agents even more.

Conclusion

These are just a few of the things I’ve noticed ReactiveScript improve in the authoring experience. Let’s be honest though, AI is doing a lot of the writing of the code these days so the fewer gotchas it can run into the better.

Am I missing any? Let me know.