Core Concepts
State & Reactivity
Reactivity in Wizz is plain JavaScript: declare state with let, change it with assignment. The compiler finds the assignments, the analyzer knows which nodes read each variable, and a microtask scheduler applies the changes in one pass.
Reactive state
Every top-level let in a component script is reactive. Reassigning it marks the variable as changed and schedules an update. const bindings render once.
<script>
let count = 0; // reactive
let user = { name: "Ada" }; // reactive
const LIMIT = 10; // constant: renders once
function increment() {
if (count < LIMIT) {
count += 1; // marks count, schedules one update
}
}
function rename(next) {
user = { ...user, name: next }; // reassign objects immutably
}
</script>
<main>
<p>{user.name} clicked {count} times</p>
<button on:click={increment}>+1</button>
</main>Microtask batching
Updates are asynchronous and batched. Each assignment only marks its variable; one DOM update runs on the next microtask with every mark combined. A handler that changes several variables triggers a single update, and the DOM settles before the browser paints.
function reset() {
count = 0; // mark count
label = "Reset"; // mark label
history = []; // mark history
// nothing has touched the DOM yet...
}
// ...one update runs on the next microtask with all three marks combined.
async function afterReset() {
reset();
await Promise.resolve(); // let the microtask flush
// the DOM now reflects every change
}Which mutations are tracked
A syntax-aware rewriter inserts the scheduler call after statement-level mutations, following them into function and arrow callback bodies. Mutations in expression positions are deliberately left alone.
// Tracked: statement-level assignments and updates, also inside callbacks
count = count + 1;
count += 1;
count++;
user.name = "Grace"; // property chains mark the root (user)
items = [...items, next];
setTimeout(() => { status = "done"; }, 500);
onMount(() => { ready = true; });
// Not tracked: method calls, expression positions, unbraced bodies
items.push(next); // no assignment: reassign the array instead
log(count = 5); // assignment inside a call argument
if (done) status = "ok"; // unbraced control-flow body: add bracesEvent handling
on:<event> binds any native DOM event name — click, input, submit, keydown, pointermove — with addEventListener. The value is either a component function or an expression that receives the event.
<script>
let query = "";
let count = 0;
function handleInput(event) {
query = event.target.value;
}
function add(amount) {
count += amount;
}
function handleSubmit(event) {
event.preventDefault();
}
</script>
<form on:submit={handleSubmit}>
<input value={query} on:input={handleInput} />
<button type="button" on:click={(event) => add(5)}>+5</button>
</form>- Event directives work on native elements only — pass callbacks to components as props.
- Handlers are client-only; server rendering skips them, and hydration attaches them after the adoption walk passes.
destroy()removes every listener Wizz attached.
Lists with {#each}
Give each row a key when node identity should follow the item — for example when rows reorder. On reassignment Wizz retains matching keys, moves them into the new order, updates their text and attributes, creates new keys, and removes missing ones. Duplicate keys throw.
<script>
let todos = [
{ id: 1, title: "Read the contract", rowClass: "item is-done" },
{ id: 2, title: "Build a keyed list", rowClass: "item" }
];
// {#each} bodies take no on: directives — delegate from the container.
function handleListClick(event) {
const row = event.target.closest("[data-id]");
if (!row) return;
const id = Number(row.dataset.id);
todos = todos.filter((todo) => todo.id !== id);
}
</script>
<div class="todos">
<ul on:click={handleListClick}>{#each todos as todo (todo.id)}<li class={todo.rowClass} data-id={todo.id}>{todo.title}</li>{/each}</ul>
</div>For primitives, or when position is identity enough, omit the key. A keyless block updates rows in place by index, appends as it grows, and trims as it shrinks.
<script>
let tags = ["compiler", "ssr", "zero-deps"];
</script>
<ul class="tags">{#each tags as tag}<li>{tag}</li>{/each}</ul>Every {#each} must…
sit inside a native element, have exactly one native root element per row, and iterate a reactive let to update (a const renders once).
Row bodies may not contain…
imported components, on: directives, or nested {#if}/{#each} blocks. Precompute row classes and labels, and delegate events.
Live: keyed todo list
A keyed list with one delegated click listener, a submit handler, CSS-driven filtering, and persist() — add a few items and reload.
Live · TodoDemo.wizz
- Read the component contract
- Build a keyed list
- Ship with zero dependencies
Conditional rendering
{#if} chooses a branch with the expression grammar — identifiers, member access, literals, and strict equality — plus an optional {:else}.
<script>
export let section = "About";
</script>
<div class="panel">
{#if section === "About"}
<p>The counter is available.</p>
{:else}
<p>Unknown section.</p>
{/if}
</div><script>
let open = false;
let panelClass = "panel";
function toggle() {
open = !open;
panelClass = open ? "panel is-open" : "panel";
}
</script>
<section class="disclosure">
<button on:click={toggle} aria-expanded={open}>Details</button>
<div class={panelClass}>Shown and hidden with CSS, updated reactively.</div>
</section>
<wizz:style>
.panel { display: none; }
.panel.is-open { display: block; }
</wizz:style>persist(): cross-tab state
Initialize a top-level let with persist(key, default) and it survives reloads and stays in sync across tabs, with no other change to how you use it.
<script>
let theme = persist("theme", "light");
function toggle() {
theme = theme === "light" ? "dark" : "light";
}
</script>
<main>
<button on:click={toggle}>Theme: {theme}</button>
</main>- Mount: the stored value (JSON) wins over the default; unreadable values fall back to the default.
- Write: every statement mutation writes back. Quota and private-mode failures leave in-memory state intact.
- Sync: mounts subscribe to a shared per-page bus; tabs converge over
BroadcastChannel, falling back tostorageevents. A tab's own writes never echo back. - Server: there is no storage, so SSR renders the default and hydration restores the stored value — derive dependent values in
onMount. - Keys are string literals used verbatim: two components persisting the same key share state on purpose.
Live: persisted counter
This is the same wizz_docs_counter key as the landing page hero. Change it here, then open the home page in another tab.
Live · Counter.wizz
Shared with the hero
Patterns
Derived values
Wizz has no computed-value syntax: keep a single function that recomputes derived state, call it after the mutations that matter, and once on mount.
<script>
let items = [];
let total = 0;
let summary = "Cart is empty";
// One function owns every derived value; call it after each change.
function recompute() {
total = items.reduce((sum, item) => sum + item.price, 0);
summary = items.length === 0 ? "Cart is empty" : items.length + " items";
}
function add(item) {
items = [...items, item];
recompute();
}
onMount(recompute); // also after hydration restores persisted state
</script>Cleanup
Anything you start in onMount — intervals, observers, global listeners, AbortControllers — stop in onDestroy. Do not change state there; the component is being torn down.