[]previous and next page

The rules

Each rule below has a program that writes a signal and prints the rendered text before and after.

For agentsUpdated
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

Each rule below has a program that writes a signal and prints the rendered text before and after. flushSync() applies the write immediately so the output is deterministic.

Derived constants#

double reads count(), so the compiler turns it into a derived value. {double} and {double()} both update at runtime: the derived value is readable and callable. Only the first form type-checks, because TypeScript sees a number; the second exists so that code written with accessors (const double = () => count() * 2) keeps working when it is changed to a constant. Write {double}.

src/derived.tsxtsx
import { flushSync, useStore } from "@shadow-js/core";
import { render } from "@shadow-js/web";

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

function Counter() {
  const double = count() * 2;
  return (
    <p>
      {count()} x 2 = {double} /{" "}
      {/* @ts-expect-error a number is not callable for TypeScript; the compiler makes the call work */}
      {double()}
    </p>
  );
}

const host = document.createElement("div");
const dispose = render(() => <Counter />, host);
console.log(host.textContent);
setCount(1);
flushSync();
console.log(host.textContent);
dispose();
Expected output
0 x 2 = 0 / 0
1 x 2 = 2 / 2

Destructured props#

Each destructured name follows props. A default applies only when the prop is undefined, so 0, "" and false pass through.

src/destructured.tsxtsx
import { flushSync, useStore } from "@shadow-js/core";
import { render } from "@shadow-js/web";

const [title, setTitle] = useStore("a");
const [size, setSize] = useStore<number | undefined>(undefined);

function Label(props: { title: string; size?: number | undefined; label: string }) {
  const { title, size = 3, label: text } = props;
  return (
    <p>
      {title} {size} {text}
    </p>
  );
}

const host = document.createElement("div");
const dispose = render(() => <Label title={title()} size={size()} label="x" />, host);
console.log(host.textContent);
setTitle("b");
flushSync();
console.log(host.textContent);
setSize(0);
flushSync();
console.log(host.textContent);
dispose();
Expected output
a 3 x
b 3 x
b 0 x

Destructured parameters#

The same rule for a destructured parameter:

src/parameter.tsxtsx
import { flushSync, useStore } from "@shadow-js/core";
import { render } from "@shadow-js/web";

const [name, setName] = useStore<string | undefined>(undefined);

function Row({ id, name = "anon" }: { id: number; name?: string | undefined }) {
  return (
    <p>
      {id} {name}
    </p>
  );
}

const host = document.createElement("div");
const dispose = render(() => <Row id={1} name={name()} />, host);
console.log(host.textContent);
setName("Ada");
flushSync();
console.log(host.textContent);
dispose();
Expected output
1 anon
1 Ada

Chains#

A constant that reads a derived constant is derived too, even if its own expression has no call or member access:

src/chain.tsxtsx
import { flushSync, useStore } from "@shadow-js/core";
import { render } from "@shadow-js/web";

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

function Chain() {
  const a = count();
  const b = a * 10;
  return (
    <p>
      {a} {b}
    </p>
  );
}

const host = document.createElement("div");
const dispose = render(() => <Chain />, host);
console.log(host.textContent);
setCount(3);
flushSync();
console.log(host.textContent);
dispose();
Expected output
0 0
3 30

Derived functions#

When a derived value is itself a function, calling it calls that function, so it stays callable:

src/function.tsxtsx
import { flushSync, useStore } from "@shadow-js/core";
import { render } from "@shadow-js/web";

const [mode, setMode] = useStore<"up" | "down">("up");
const up = (text: string) => text.toUpperCase();
const down = (text: string) => text.toLowerCase();

function Shout() {
  const f = mode() === "up" ? up : down;
  return <p>{f("Ab")}</p>;
}

const host = document.createElement("div");
const dispose = render(() => <Shout />, host);
console.log(host.textContent);
setMode("down");
flushSync();
console.log(host.textContent);
dispose();
Expected output
AB
ab

Returned conditionals#

A returned ternary or logical expression follows its condition. Without the rewrite, the component would return one branch and keep it forever.

src/returned.tsxtsx
import { flushSync, useStore } from "@shadow-js/core";
import { render } from "@shadow-js/web";

const [open, setOpen] = useStore(true);
const [item, setItem] = useStore<string | undefined>("Ada");

function Gate() {
  return open() ? <b>open</b> : <i>closed</i>;
}

function Item(props: { item?: string | undefined }) {
  return props.item && <u>{props.item}</u>;
}

const gate = document.createElement("div");
const row = document.createElement("div");
const disposeGate = render(() => <Gate />, gate);
const disposeRow = render(() => <Item item={item()} />, row);
console.log(`${gate.textContent}|${row.textContent}`);
setOpen(false);
setItem(undefined);
flushSync();
console.log(`${gate.textContent}|${row.textContent}`);
disposeGate();
disposeRow();
Expected output
open|Ada
closed|