Guide

Reactivity

A component's code runs once. What it declares with state is a signal; what reads a signal (a let, a statement, a place in the markup) is recorded as it runs; and when the signal is assigned a different value, those, and only those, are worked out again. The component does not render again, nothing is compared against a virtual DOM, and there are no dependency lists to keep: what code reads is what it follows. This is how SolidJS and Svelte's runes work, not React.

component Cart(items: list) {    state lines = items                          // a signal    state coupon = ""                            // another    let total = lines | map((l) => l.price * l.qty) | sum()    // follows lines    let off = coupon == "TEN" ? round(total * 0.1, 2) : 0     // follows coupon and total    if total > 1000 { every(5000, () => { notify("Big order!") }) }   // starts and stops with it    <div>        <ul>{for l in lines => <li key={l.id}>{l.name} × {l.qty}</li>}</ul>        <input value={coupon} placeholder="Coupon"/>        <p class={off > 0 && "saved"}>Total {total - off}</p>        <button onclick={() => { lines = lines | filter((l) => l.qty > 0) }}>Tidy</button>    </div>}

Typing in the coupon box sets coupon: off is worked out again, then the text of the total and its class, and nothing else. total is not, since lines did not change; the list is not touched. The page shows the new total once the keystroke's handler is done.

What follows what

Written in a componentBecomesIs worked out again
state x = v (or ~x = v)a signal, set oncenever: it changes when assigned
let y = ... that reads something that changesa derived valuewhen something it read changed, and only once something reads it
let y = ... that reads nothing that changesa constantnever
let y = ... whose name is assigned somewhere (y = 5)a plain variablenever by itself
a statement that reads something that changes (if running { every(...) })an effectafter what it read changed
an assignment alone in the body (count = len(items), n += 1)a step as it startsnever: it runs once, and quillo check warns when it reads a prop or another state (write let)
{expr} in markup, an attribute's or a class's {...}a place on the pagewhen what it read changed; text that comes out the same is not written
cond ? <a/> : <b/>, if, match, cond && <x/> in markupa branchmade again only when another branch is chosen
for x in xs => <row/>a row for each itemrows are kept by key (else by place) and given their new item
<Child prop={x}/>props passed livethe child's own reads of prop follow x; neither component runs again
key={k} on an element or component outside a loopa part made afreshwhen k changes
@event={...}, @mount, a function given to a librarya handlerwhen it is called; what it reads is read then, not followed

"Something that changes" is: a state, a prop, a let that is itself derived, a for row's item, a live(...) state, url(), and a call of the component's own functions or the program's (taken as reading what changes, since they may), however it is written: x | show and show(x / 2) follow what show reads, as show(x) does. Builtins on constant values are not: let started = now() is the time the component started, and stays so. What is followed is what a run actually read: let shown = open ? details : summary follows open and details while open is true, and open and summary while it is false.

When the page changes

What a statement starts, it stops

What runs again owns what it started: every, listen, channel.open, the statements and branches inside it, and the functions @mount returned. Before it runs again, and when its component, row or branch leaves the page, those are stopped. So timers and streams are started by state rather than by hand:

component Clock() {    state running = false    state speed = 1000    state ticks = 0    if running { every(speed, () => { ticks += 1 }) }    // stops when running is false,    <div>                                                 // starts over when speed changes        <button onclick={() => { running = !running }}>{running ? "Stop" : "Start"}</button>        <button onclick={() => { speed = speed / 2 }}>Faster</button>        <span>{ticks}</span>    </div>}

A handler may start one too, which then lasts as long as its component, or set the state that the statement reads. For once, later, run_after(ms, f). There is no setTimeout; a debounce or a throttle is an every under a state flag that its tick turns off.

Lists and rows

Branches

if, ? :, match and cond && <markup> choose a branch, and make it again only when another is chosen. While one stays, what is inside it follows its values like anything else: an input in it keeps its focus and what was typed. A branch that goes takes everything it made with it: its state of nested components, its timers. Keep what must outlive a branch in the component's own state. key={k} on an element or a component makes it afresh whenever k changes, which is how a page for a new product starts clean (<Product key={id} id={id}/>).

Between components

What is not followed

On the server

A component's code runs once on the server too, to write the page's HTML with the state it starts with; statements that start timers or streams do nothing there. The browser then takes the server's elements as its own, from that same state, without running the initializers again (see Components). From then on, the browser is where reactivity happens.

The server has two of its own, which follow the same idea of following what was read:

Coming from other frameworks

ReactSvelte 5VueQuillo
useState$staterefstate x = v
useMemo$derivedcomputedlet y = ... reading state
useEffect (and its cleanup)$effectwatchEffecta statement reading state; what it starts stops with it
useRefbind:thistemplate refonmount={(el) => { box = el }}
value with onChangebind:valuev-modelvalue={x}: two-way by itself when x is state
useContextsetContextprovidenone: props
key{#each ... (key)}:keykey={...}
React.memo, useCallbacknot needed: nothing renders again
a list of dependenciesnone: what is read is followed

The rules of hooks have no counterpart: state is found by its name, not by the order of calls, and a statement that starts something may sit inside an if (that is how it is stopped).

On quillolang.org's Reactivity page, live demos show each of these running beside its code.

See it run

The demos below are Quillo components, running in your browser beside their code.

Watch it happen. Right-click a demo and choose Inspect (or press F12, ⌥⌘I on a Mac) to open the Elements panel, then use the demo's buttons. The panel briefly highlights each node as it changes: only the numbers and words that changed light up, never the whole demo. (The first click in a demo can light up a few more, once: that is the browser taking over the HTML the server sent.)

A state and a let that follows it

state declares a value the component keeps; let double = count * 2 follows it. In the markup, {count} and {double} are the only places that change.

counter.qwe
component CounterDemo() {    state count = 0    let double = count * 2      // follows count    <div.demo-box>        <p>Count <b>{count}</b>, doubled <b>{double}</b>.</p>        <p.muted>This sentence reads no state, so it never changes.</p>        <button onclick={() => { count += 1 }}>Add one</button>        <button onclick={() => { count = count }}>Assign the same value</button>    </div>}

Count 0, doubled 0.

This sentence reads no state, so it never changes.

A statement that starts a timer

if running { every(...) } reads running and speed. Start sets running, so the statement runs and the timer starts. Changing the speed runs it again, and the timer starts over at the new speed. Stop makes the if false, and the timer is gone.

timer.qwe
component ReactivityTimerDemo() {    state running = false    state speed = 2             // ticks a second    state ticks = 0    if running {        // runs again when `running` or `speed` changes        every(1000 / speed, () => { ticks += 1 })    }    <div.demo-box>        <p>Ticks <b>{ticks}</b> at <b>{speed}</b> a second.</p>        <button onclick={() => { running = !running }}>{running ? "Stop" : "Start"}</button>        <button onclick={() => { speed = speed == 2 ? 10 : 2 }}>            Speed {speed == 2 ? "up" : "down"}        </button>    </div>}

Ticks 0 at 2 a second.

Rows that move

Type a note into a row, then reverse the list:

rows.qwe
component RowsDemo() {    state items = [        { id: 1, text: "Apples" },        { id: 2, text: "Bread" },        { id: 3, text: "Coffee" },    ]    let add = () => {        let id = len(items) + 1        items = items + [{ id, text: `Item ${id}` }]    }    let shout = (id) => {        items = items | map((i) => i.id == id ? merge(i, { text: upper(i.text) }) : i)    }    <div.demo-box>        <ul>{for i in items =>            <li key={i.id}>{i.text} <input placeholder="a note"/></li>}</ul>        <button onclick={() => { items = reverse(items) }}>Reverse</button>        <button onclick={() => shout(2)}>Change Bread</button>        <button onclick={add}>Add</button>    </div>}
  • Apples
  • Bread
  • Coffee

A branch

branch.qwe
component BranchDemo() {    state signed_in = false    state name = "Ada"    <div.demo-box>        {signed_in            ? <p>Hello, <b>{name}</b>. <input placeholder="type, then sign out"/></p>            : <p>Signed out.</p>}        <button onclick={() => { signed_in = !signed_in }}>            {signed_in ? "Sign out" : "Sign in"}        </button>        <button onclick={() => { name = name == "Ada" ? "Grace" : "Ada" }}>            Change name        </button>    </div>}

Signed out.

Props passed live

props.qwe
component Child(value: int) {    state clicks = 0                        // the child's own    <p>        Parent says <b>{value}</b>; clicked here <b>{clicks}</b>        <button onclick={() => { clicks += 1 }}>Click</button>    </p>}component PropsDemo() {    state n = 1    state version = 1    <div.demo-box>        <Child value={n}/>        <Child key={version} value={n}/>        <button onclick={() => { n += 1 }}>Parent: add one</button>        <button onclick={() => { version += 1 }}>New key for the second</button>    </div>}

Parent says 1; clicked here 0

Parent says 1; clicked here 0

Thread
16Useful? Like it, or add to its thread below.

Thread 0 posts

On Reactivity: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.

    Edit this page on GitHub · made from the user guide · questions on Discord or r/quillo