[]previous and next page

Forgiving reactivity

Solid code keeps working, and code that Solid would break works too.

For agentsUpdated

Solid code keeps working, and code that Solid would break works too. Inside a component (a PascalCase function with at most one parameter that contains JSX), the compiler keeps these live without any ceremony (docs/contracts/forgiving-reactivity.md in the repository):

You write Meaning
const double = count() * 2; {double} or {double()} a derived value; both forms are reactive
const { title, size = 3, label: text } = props; each name follows props (defaults only when undefined)
function Row({ id, name = "anon" }) {…} same, for destructured parameters
const b = a * 10; where a is derived chains are derived too
const f = mode() === "up" ? up : down; {f("x")} a derived function stays callable
return open() ? <A /> : <B />; return props.item && <C />; the returned branch follows the condition

Rules (so it stays predictable):

  • Only const declarations at the top level of the component body.
  • Left alone: hooks and primitives (use*, create*, batch, untrack, onMount, …), functions, classes, JSX, literals, await, and anything not read in JSX or in a closure.
  • A derived value is a memo: it re-runs only when the signals it read change, and caches.
  • A name that is re-declared below the component's top level (a shadowing parameter, a block const) is not rewritten.
  • Calling a derived const (double()) returns its value; if the value is a function it is called.
  • A returned conditional or logical expression is made live only when a branch contains JSX and the condition reads something (a call, a member such as props.open, or a derived constant). Early returns (if (x()) return <A />; return <B />;) are left as written: use <Show> for those. A constant condition is never wrapped; /*@static*/ before the condition opts out.
  • Switch off per file/project with autoReactive: false (compiler) / shadow({ autoReactive: false }) (Vite).

A Solid developer can ignore all of this and write createMemo style (useMemo) everywhere; both styles compile to the same runtime calls. Two things stay Solid-like on purpose: functions whose name is not PascalCase are not rewritten, and an early return is not made live.

src/forgiving.tsxtsx
import { useStore } from "@shadow-js/core";

export function Forgiving() {
  const [count, setCount] = useStore(1);
  const double = count() * 2; // Solid: static. ShadowJS: derived.
  const size = count() > 2 ? "big" : "small";
  const total = double + count(); // chains are derived too
  return (
    <button type="button" onClick={() => setCount((n) => n + 1)}>
      {count()} / {double} {size} {total}
    </button>
  );
}