<let> vs <const>
<let>: mutable reactive state<const>: derived value, pure, recomputes with dependenciesstatic const: module-level constant, initialized once, not reactive
A .marko template has a few ways to introduce a variable, and the choice describes how the value updates: a <let> is reassigned and everything reading it reacts, while a <const> recomputes when its dependencies change. Sometimes a reactive tag variable is not required at all; a value that never changes belongs in a static statement instead, evaluated once when the module loads.
Mutable State
The <let> tag declares state written directly, typically from event handlers. Assigning to its tag variable updates every expression that reads it.
<let/tipPercent=20>
<button onClick() {
tipPercent = 18;
}>18%</button>
<button onClick() {
tipPercent = 22;
}>22%</button>
<output>Tip: ${tipPercent}%</output><let/tipPercent=20>
<button onClick() { tipPercent = 18 }>18%</button>
<button onClick() { tipPercent = 22 }>22%</button>
<output>Tip: ${tipPercent}%</output>let/tipPercent=20
button onClick() {
tipPercent = 18;
} -- 18%
button onClick() {
tipPercent = 22;
} -- 22%
output -- Tip: ${tipPercent}%let/tipPercent=20
button onClick() { tipPercent = 18 } -- 18%
button onClick() { tipPercent = 22 } -- 22%
output -- Tip: ${tipPercent}%Clicking a button reassigns tipPercent, and the <output> text updates to match. Nothing else re-renders.
The value= attribute is only an initial value. Once created, the state changes solely through assignment (or a valueChange handler), even if the initializing expression later changes.
Derived Values
The <const> tag declares a value computed from other reactive variables. It cannot be assigned; it recomputes whenever anything it reads changes.
<let/bill=64>
<let/tipPercent=20>
<const/tip=(bill * (tipPercent / 100))>
<button onClick() {
tipPercent += 1;
}>Round up</button>
<output>Tip: $${tip.toFixed(2)}</output><let/bill=64>
<let/tipPercent=20>
<const/tip=bill * (tipPercent / 100)>
<button onClick() { tipPercent += 1 }>Round up</button>
<output>Tip: $${tip.toFixed(2)}</output>let/bill=64
let/tipPercent=20
const/tip=(bill * (tipPercent / 100))
button onClick() {
tipPercent += 1;
} -- Round up
output -- Tip: $${tip.toFixed(2)}let/bill=64
let/tipPercent=20
const/tip=bill * (tipPercent / 100)
button onClick() { tipPercent += 1 } -- Round up
output -- Tip: $${tip.toFixed(2)}Here tip always reflects the current bill and tipPercent. No handler keeps it in sync; the relationship is declared once.
Mirroring a derivation into a <let> and updating it by hand is an anti-pattern:
<let/bill=64>
<let/tipPercent=20>
<let/tip=(bill * (tipPercent / 100))>
<button onClick() {
tipPercent += 1;
tip = bill * (tipPercent / 100); // ❌ BAD: repeated in every handler
}>
Round up
</button><let/bill=64>
<let/tipPercent=20>
<let/tip=bill * (tipPercent / 100)>
<button onClick() {
tipPercent += 1;
tip = bill * (tipPercent / 100); // ❌ BAD: repeated in every handler
}>
Round up
</button>let/bill=64
let/tipPercent=20
let/tip=(bill * (tipPercent / 100))
button onClick() {
tipPercent += 1;
tip = bill * (tipPercent / 100); // ❌ BAD: repeated in every handler
} --
Round uplet/bill=64
let/tipPercent=20
let/tip=bill * (tipPercent / 100)
button onClick() {
tipPercent += 1;
tip = bill * (tipPercent / 100); // ❌ BAD: repeated in every handler
} --
Round upEvery code path touching bill or tipPercent must now also recompute tip, and forgetting one shows a stale total. A <const> states the formula once.
Because updates are batched, a derived value read inside an event handler still holds the result computed from the previous state. See Stale Derived Values.
Module Constants
Statements prefixed with static run in module scope, once per environment when the template loads. A static const is shared by every instance, across every render and request.
static const currencyFormat = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD"
});
<let/bill=64>
<let/tipPercent=20>
<const/total=(bill * (1 + tipPercent / 100))>
<output>${currencyFormat.format(total)}</output>static const currencyFormat = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
});
<let/bill=64>
<let/tipPercent=20>
<const/total=bill * (1 + tipPercent / 100)>
<output>${currencyFormat.format(total)}</output>static const currencyFormat = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD"
});
let/bill=64
let/tipPercent=20
const/total=(bill * (1 + tipPercent / 100))
output -- ${currencyFormat.format(total)}static const currencyFormat = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
});
let/bill=64
let/tipPercent=20
const/total=bill * (1 + tipPercent / 100)
output -- ${currencyFormat.format(total)}The Intl.NumberFormat instance is built a single time, however many components render. That makes static const the right home for formatters, configuration, lookup tables, and helper functions.
Static statements are evaluated only once, outside the reactive system. Anything that varies per instance or over time belongs in <const> or <let>.
A value that reads input or any tag variable must be a <const>, even if it never appears to change; only a <const> is initialized per instance and tracked.
Choosing
Prefer the most fixed declaration that still describes the value:
- Assigned by handlers or otherwise changes on its own:
<let> - Computed from
input, tag variables, or other reactive state:<const> - Identical for every instance and never changes:
static const
This is not only a style choice: the compiler generates update code only for what can actually change, so the more fixed forms produce less work in the browser.
Contributors
Helpful? You can thank these awesome people! You can also edit this doc if you see any issues or want to improve it.