Reactivity Reference

Marko's goal is to make it easy to represent performant experiences with rich client interactions. This is enabled through its reactivity system. The reactive system is how Marko determines what needs to update and when.

The core of Marko's reactive system is the <let> tag.

Reactive Variables

In Marko, Tag Variables, Tag Parameters, and input are all reactive. This means they are tracked by the Marko compiler and when these values are caused to update any dependent render expressions are also updated.

Render Expressions

Any expression within a .marko template that references a reactive variable is considered reactive and will be updated alongside that variable.

These reactive expressions may exist throughout the template in attributes, dynamic text, dynamic tag names, and script content.

Note

All JavaScript expressions within the Marko template may be reactive with the exception of static statements (including import, export, static, server and client) which are evaluated once when the template is loaded.

<let/count=0>
<button onClick() {
  count++;
}>Current: ${count}</button>
<let/count=0>

<button onClick() { count++ }>Current: ${count}</button>
let/count=0
button onClick() {
  count++;
} -- Current: ${count}
let/count=0

button onClick() { count++ } -- Current: ${count}

Here, a count Tag Variable is mutated by a button click. Because the text content of the button references count, it is automatically kept in sync with the new value.

Caution

In some cases Marko may cause some expressions to evaluate together. This is why render expressions should be pure.

Tip

Marko is a compiled language, and its reactive graph is discovered at compile time instead of during runtime. This is in contrast with many of the other leading approaches, such as Signals in SolidJS and Hooks in React.

Scheduling Updates

Marko automatically batches work to ensure optimal performance. Any time a reactive variable is changed, its update is queued to ensure that multiple changes will be applied efficiently together.

This update queue is typically scheduled after a microtask.

If additional updates are scheduled after the queue is consumed but before the update is painted, they are deferred until the next frame. This accomplishes a few things:

  • Content ready to display to the user is not blocked.
  • It is not possible to lock up the application in an infinite update loop.
  • The update loop can be used to power animations (although CSS Animations & Transitions/ JS Web Animations API are preferred in most cases).

Stale Derived Values

Assigning a <let> writes the new value into scope immediately, so the tag variable reads back the new value on the next line. Everything derived from it, including <const> tag variables and the rendered output, is recomputed only when the queue is flushed, so for the remainder of the handler those values are still the ones computed from the previous state.

<let/quantity=1>
<const/subtotal=(quantity * input.unitPrice)>
<button onClick() {
  quantity++;

  // `quantity` is already the new value, so the reported total is
  // recomputed from it rather than read from `subtotal`.
  input.onQuantityChange(quantity, quantity * input.unitPrice);
}>
  Add one
</button>
<output>${subtotal}</output>
<let/quantity=1>
<const/subtotal=quantity * input.unitPrice>

<button onClick() {
  quantity++;

  // `quantity` is already the new value, so the reported total is
  // recomputed from it rather than read from `subtotal`.
  input.onQuantityChange(quantity, quantity * input.unitPrice);
}>
  Add one
</button>

<output>${subtotal}</output>
let/quantity=1
const/subtotal=(quantity * input.unitPrice)
button onClick() {
  quantity++;

  // `quantity` is already the new value, so the reported total is
  // recomputed from it rather than read from `subtotal`.
  input.onQuantityChange(quantity, quantity * input.unitPrice);
} --
  Add one
output -- ${subtotal}
let/quantity=1
const/subtotal=quantity * input.unitPrice

button onClick() {
  quantity++;

  // `quantity` is already the new value, so the reported total is
  // recomputed from it rather than read from `subtotal`.
  input.onQuantityChange(quantity, quantity * input.unitPrice);
} --
  Add one

output -- ${subtotal}
Warning

Passing subtotal here would report the total for the previous quantity. Reading the rendered total back out of the DOM has the same problem, since the DOM has not been updated yet.


Contributors

Helpful? You can thank these awesome people! You can also edit this doc if you see any issues or want to improve it.