The rules
Each rule below has a program that writes a signal and prints the rendered text before and after.
| 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}.
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();
0 x 2 = 0 / 0
1 x 2 = 2 / 2Destructured props#
Each destructured name follows props. A default applies only when the prop is undefined, so 0, "" and
false pass through.
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();
a 3 x
b 3 x
b 0 xDestructured parameters#
The same rule for a destructured parameter:
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();
1 anon
1 AdaChains#
A constant that reads a derived constant is derived too, even if its own expression has no call or member access:
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();
0 0
3 30Derived functions#
When a derived value is itself a function, calling it calls that function, so it stays callable:
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();
AB
abReturned conditionals#
A returned ternary or logical expression follows its condition. Without the rewrite, the component would return one branch and keep it forever.
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();
open|Ada
closed|