Module · Level 1 · proposed shapes

Reactivity & Data

Reactivity is a declared dependency graph in markup: local state, derived values, and external resources, each with a distinct lifecycle. The graph is statically analyzable, so it lowers to React state, Vue refs, Svelte runes, or a signal-based browser runtime: one semantics, many backends, no eval().

Proposed direction · shapes open for review

Reactive sources

HTML Next keeps a few concepts separate rather than overloading one element, because their lifecycles differ. All of them are declarations: they live in the definition's <defs> region, not in the visible markup (see Components).

ElementIsWritten by
<state name :value>a local mutable value<set> in a handler, or bind:
<computed name from>a pure value derived from othersnothing, recomputed from its dependencies
<data name src>a resource read from a sourceits params; refetched reactively
<state name="query" value="">        <!-- literal string -->
<state name="page" :value="1">        <!-- number, via the colon -->
<computed name="hasQuery" from="query != ''">  <!-- derived boolean -->

The design, a declared dependency graph that can be analyzed statically rather than traced at runtime, has deep prior art in signals and fine-grained reactivity5: Solid signals and createMemo and Angular signals are its closest current relatives, with Knockout observables as the historical ancestor. RxJS is a deliberate contrast, it models push streams, not the settled value cells <state> and <computed> are.

The colon carries the type

<set> mirrors <state> exactly: same name, same :value. A plain attribute is a string; the : prefix makes the value a typed expression. :value="false" is the boolean, value="false" the five-character string.

<data>: a declared, reactive read

This is HTML Next's standards-shaped answer to htmx1: instead of hx-get/hx-trigger/hx-target string attributes swapping opaque HTML, a <data> element declares a typed, reactive resource whose parameters are visible right where it lives. Each <param :value> subscribes to the state it binds, so the set of params is the dependency graph: change one and the resource refetches, and bindings that read it re-render. Nothing triggers it imperatively. This is the surface Solid createResource already ships6, a resource whose fetch re-runs on source change and exposes loading and error; TanStack and Vue Query are the same idea with a params-keyed cache.

<data name="search" src="/api/search" type="SearchResults" debounce="200ms">
  <param name="q" :value="query">     <!-- subscribes to query: refetches when it changes -->
  <param name="page" :value="page">
</data>

Params are serialized, never interpolated into a URL string. A {name} placeholder in src is filled from the matching <param> (RFC 6570 URI Templates)2; params not named in the template become the query string. There is no string concatenation, and therefore no injection surface.

A <data> exposes a small, typed surface any binding can read:

PathMeaning
search.pendinga request is in flight
search.valuethe resolved value (typed as type)
search.errorthe failure, if any
search.oksettled with a value and no error

To refetch with unchanged inputs (a manual refresh, polling) there is no imperative call: bump a state param the source depends on, or declare a poll interval. A param change cancels any in-flight request, and requests are keyed by their resolved params, giving a natural cache key.

Locality of behaviour, end to end
<input bind:value="query" placeholder="Search…">
<template $match>
  <progress $when="search.pending"></progress>
  <output $when="search.error"><value of="search.error.message"></value></output>
  <ul $else>
    <li $each="r of search.value.results" $key="r.id"><value of="r.title"></value></li>
  </ul>
</template>

Writes: the native form

Reactivity covers reads completely, but a write has an irreducible moment, the user commits, and you do not POST on every keystroke. The platform already has the declarative write primitive: form submission3. A write is a <form> with the same param model as <data>, async-enhanced (no full-page navigation), exposing the identical .pending/.error surface. The only difference from a read is when it sends: a read sends when a param changes, a write sends on submit.

<!-- a write is the native form primitive, async-enhanced -->
<form name="save" method="post" src="/api/posts/{id}" on:success="afterSave">
  <param name="id" :value="post.id">        <!-- {id} → path -->
  <input name="title" bind:value="draft.title">  <!-- a control → body -->
  <param name="tags" :value="draft.tags">   <!-- no control → body -->
  <button>Publish</button>
</form>

<!-- save.pending / save.error read exactly like a <data> source -->
<p aria-live="polite" $if="save.pending">Saving…</p>

Named inputs and <param> serialize the same way, routed by method: to the query string for GET, to the body for POST/PUT/PATCH, exactly as a native form already does. Post-write side effects (leave edit mode, announce success) belong to the form's lifecycle, so they run from an on:success handler, not inline.

Open for review

Settled: reactive reads, form writes, param-driven dependency tracking. Still open: pagination accumulation, optimistic updates, revalidation policy, request headers/auth, and real-time push (SSE/WebSocket) where the server drives change without a param bump. Outside-world reactions (timers, subscriptions) are now the JavaScript layer's job, see Lifecycle below.

Lifecycle

In a reactive component most of what framework lifecycle callbacks did is absorbed by the dependency graph, so HTML Next needs far fewer hooks, and names the ones it keeps after the platform's custom-element reactions, for least surprise.

What the graph already handles

  • Prop / attribute changes: the bindings, <computed>, and <data> that read a prop re-run automatically. This is attributeChangedCallback, and you never write it.
  • Fetch on mount, refetch on change: declare a <data>; it runs when its params resolve and again when they change. This is the connectedCallback fetch.
  • Initial and derived state: <state :value> and <computed>; initial focus is autofocus.

Declarative lifecycle events

For a reaction that is not a derivation, on:connect and on:disconnect run a handler when the component is connected or disconnected, mirroring connectedCallback/disconnectedCallback7 and firing again on reconnect. Because handlers are declarative, they set state or <dispatch> an event, with no imperative code. They are client-only: SSR renders the static tree, and hydration is what connects, so nothing lifecycle-driven runs on the server.

<defs>
  <state name="visible" :value="false">
  <handler name="show"><set name="visible" :value="true"></handler>
  <handler name="hide"><set name="visible" :value="false"></handler>
</defs>

<!-- lifecycle events run handlers; client-only (SSR never connects) -->
<section on:connect="show" on:disconnect="hide"></section>
Web Components reactionHTML Next
constructornone, the <template component> declaration is the definition
connectedCallbackon:connect (declarative) · the JS behavior's connect (imperative, below)
disconnectedCallbackon:disconnect · the JS behavior's teardown
attributeChangedCallbacknot written, reactivity re-runs the dependents
adoptedCallbackon:adopt (rare, cross-document moves)
form-associated callbacksthe forms & validation story (see Validation)
Why connect, not mount

Framework developers will read this as mount / unmount (React, Vue, Svelte), and that is the right analogy, with one difference and one deciding reason to prefer the platform word. The difference: on:connect fires on every connect, including when an element is moved and re-inserted, not only the first time. The reason: it is exactly right for SSR. The server renders static markup where nothing is connected to a live document; on the client, hydration adopts the node and connects it, and that is when on:connect fires. "Mount" blurs that boundary; "connect" names precisely the moment an element joins a live, interactive document, which is client-side hydration and never the server.

Imperative lifecycle is the JavaScript layer

Timers, subscriptions (SSE/WebSocket), IntersectionObserver, third-party libraries, imperative animation: these are genuinely imperative, with setup and teardown, and have no declarative form. They live in the reserved JavaScript layer (see The JavaScript Layer), at a later Level, where a component may attach an ES-module controller whose connect hook returns a disposer run on disconnect, the shape connectedCallback/disconnectedCallback and React/Svelte effects already established.8 The declarative layer stays free of lifecycle ceremony; the imperative layer is the only part with a real lifecycle, and it is opt-in.

The dependency graph

Every expression exposes the paths it reads, and every <param :value> names a subscription, so the graph is known statically. This buys type-checkable expressions, predictable invalidation, ahead-of-time generation for any reactive framework, and a browser runtime that needs no dynamic code.

TargetLowers reactivity to
ReactuseState / useMemo / a resource hook
Vueref / computed / watch
Svelterunes ($state / $derived / $effect)
Browser runtimesignals (the TC39 Signals proposal as a candidate substrate)4

Element reference

<state> · <computed> local reactive values

Attributes
<state name :value> · <computed name from>
Semantics
state is mutated only by <set>/bind:; computed is pure and recomputed from dependencies.
Level
L1

<data> · <param> declared reactive read

Attributes
name, src, type?, debounce?, poll? · <param name :value>
Exposes
.pending, .value, .error, .ok
Refetch
A bound param changing refetches and cancels stale requests; keyed by resolved params. {name} in src is an RFC 6570 path param.
Level
L1 (proposed)

<form> (enhanced) declared write

Attributes
name, src, method, enctype? · inputs and <param> as body/query fields
Exposes
.pending, .error, .ok under its name, like a <data>
Sends
On submit; on:success/on:error run handlers for post-write steps.
Level
L1 (proposed)

Sources

  1. htmx (hx-get/hx-trigger/hx-target) and its Locality of Behaviour essay.
  2. IETF, RFC 6570: URI Template (the {name} placeholder syntax).
  3. WHATWG HTML, form submission (the native write primitive and its param serialization).
  4. TC39, Signals proposal (a candidate reactive substrate).
  5. Signals and fine-grained reactivity as a declared, statically-analyzable dependency graph: Solid signals and createMemo, Angular signals, with Knockout observables as the historical ancestor (and Vue ref/computed, Preact signals). Contrast: RxJS models push streams, not value cells.
  6. Solid createResource: a resource whose fetch re-runs when its source changes, exposing loading and error, the exact surface <data> exposes as .pending/.error/.value/.ok. TanStack Query and Vue Query key a cache by request params, matching requests keyed by resolved params here.
  7. WHATWG HTML, custom element reactions (connectedCallback/disconnectedCallback, the naming precedent for on:connect/on:disconnect).
  8. The return-a-disposer teardown shape: Solid onCleanup and React effect cleanup.