Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .local.dic
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ debounce
declaratively
DefinitelyTyped
deps
destroyables
destructor
destructors
dev
draggable
dropdown
Expand Down Expand Up @@ -136,6 +139,9 @@ LSP
Mapbox
TomTom
MDN
memoization
memoize
memoized
metaprogramming
misspelt
mixin
Expand Down Expand Up @@ -173,6 +179,7 @@ PRs
readme
readonly
recognizers
recomputation
recursing
Redux
relayout
Expand All @@ -196,9 +203,11 @@ screencasting
selectable
self-referentiality
serverless
Signalium
singularize
Splattributes
SSR
Starbeam
stateful
subclassed
subclasses
Expand All @@ -218,6 +227,7 @@ synergistically
syntaxes
tagless
TalkBack
TC39
teardown
template-lifecycle-dom-and-modifiers
templating
Expand All @@ -237,6 +247,7 @@ typechecker
typings
UIs
un-representable
uncached
unordered
unsilence
unstyled
Expand Down
198 changes: 198 additions & 0 deletions guides/release/in-depth-topics/reactivity/derived-state.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
Derived state is the formula layer of the reactive graph: everything computed _from_ [root state](../root-state/). In a healthy Ember application, this is most of your state—and in Ember, it requires no special API at all. An ordinary getter, an ordinary function, an ordinary template expression: if it reads tracked state, it is derived state, and it stays up to date automatically.

```js
import { tracked } from '@glimmer/tracking';

class Search {
@tracked query = '';
@tracked results = [];

get hasQuery() {
return this.query.length > 0;
}

get visibleResults() {
return this.results.filter((result) => !result.hidden);
}

get summary() {
return this.hasQuery
? `${this.visibleResults.length} results for “${this.query}”`
: 'Type to search';
}
}
```

There is no decorator on these getters, no list of dependencies, no subscription. `summary` depends on `hasQuery` and `visibleResults`, which depend on `query` and `results`—and the system discovers that graph by itself, by watching what each computation reads while it runs.

## Derivations Are Lazy

The most important thing to understand about derived state in Ember: **changing root state does not run your getters.** A write to `@tracked` state only marks the things that consumed it as out of date. The getter runs again when—and only when—something actually reads it. If nothing reads it, it never runs.

```js
class Search {
@tracked query = '';

get normalizedQuery() {
console.log('computing!');
return this.query.trim().toLowerCase();
}
}

let search = new Search();

search.query = 'Hello';
search.query = 'Hello, world';
search.query = 'Hello, world!';
// ...nothing is logged. No computation has happened at all.

search.normalizedQuery; // logs "computing!" — exactly once
```

This is _pull-based_ reactivity (described in [Thinking in Reactivity](../)), and it's why you can be generous with derived state. A getter that nothing currently displays costs nothing, no matter how often its inputs change. Ten getters reading the same tracked property add no overhead to writes. Work happens at read time, driven by what the page actually needs.

The corollary: **never rely on a getter running for its timing.** A derivation may run once, many times, or never; it may run later than you expect or more often than you expect. If you find yourself wanting "run this code _when_ X changes," you are looking for something other than derived state—see [Reactivity and the Outside World](../outside-world/).

## Derivations Must Be Pure

A derivation's job is to compute a value from its inputs. It must not _change_ anything—and above all, it must not write to tracked state. This rule is universal across reactive systems (Solid's documentation gives the same warning about memos), and Ember enforces it: writing to a tracked value that has already been read during the current render throws a development-mode error:

```text
Error: You attempted to update `count`, but it had already been used
previously in the same computation.
```

This is sometimes called the _backtracking assertion_: render evaluates your derivations top to bottom, and a write partway through would invalidate output that was already produced—the reactive equivalent of a spreadsheet formula that edits other cells. The fix is never to find a sneakier place for the write; it's to restructure so the write isn't needed:

```js
// 🛑 Don't: a "derivation" that pushes its result somewhere else
get filteredItems() {
let filtered = this.items.filter((item) => item.matches(this.query));
this.resultCount = filtered.length; // write inside a read!
return filtered;
}

// ✅ Do: derive both values independently
get filteredItems() {
return this.items.filter((item) => item.matches(this.query));
}

get resultCount() {
return this.filteredItems.length;
}
```

Purity is also what makes derived state effortless to test: `new Search()`, set some properties, assert on some getters. No rendering, no waiting, no framework.

## Caching

By default, a getter recomputes every time it is read. This surprises people coming from systems whose derivations are memoized by default (Solid's `createMemo`, Signalium's `reactive` functions)—but it's the right default, because most derivations are cheap and a cache has its own costs. Recomputing `this.items.length` is faster than checking whether a cached copy is still valid.

When a derivation _is_ genuinely expensive—sorting thousands of rows, building a chart's dataset—mark it with `@cached`:

```js
import { cached, tracked } from '@glimmer/tracking';

class Report {
@tracked transactions = [];

@cached
get sortedByAmount() {
return [...this.transactions].sort((a, b) => b.amount - a.amount);
}
}
```

A `@cached` getter remembers its result along with everything it consumed while computing it. Reads return the cached value until one of those consumed inputs is invalidated; then the next read recomputes. (Note what `@cached` does _not_ do: it doesn't compare the new result to the old one. If `transactions` is invalidated but the sorted output happens to come out identical, consumers downstream are still re-evaluated. Some systems—Solid's memos, Signalium—add an equality cutoff here; Ember today does not.)

Two more good reasons to reach for `@cached`, beyond raw cost:

- **Stable identity.** An uncached getter that returns a fresh array or object on every read can defeat downstream `===` checks and cause child components to see "new" values that are deep-equal to the old ones. Caching makes the derivation return the _same_ object until its inputs actually change.
- **Once-per-change semantics.** If a derivation must observably run at most once per change (because it allocates, logs, or is just very hot), `@cached` guarantees that.

See [Autotracking In-Depth](../../autotracking-in-depth/#toc_caching-of-tracked-properties) for a step-by-step illustration of the caching behavior.

## Composition: Build Big Formulas from Small Ones

Because derivations are just getters and functions, they compose the way all JavaScript composes—and the dependency graph follows along. Prefer many small derivations over one large one:

```js
get activeUsers() {
return this.users.filter((user) => user.isActive);
}

get activeAdmins() {
return this.activeUsers.filter((user) => user.isAdmin);
}

get headline() {
return `${this.activeAdmins.length} admins online`;
}
```

Each step is independently readable, testable, and reusable—and invalidation stays precise, because each layer only consumes what it actually reads.

Derivations don't have to live on classes, either. A plain function that reads tracked state is a derivation too, and in template tag files you can use one directly as a helper:

```gjs
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';

function initials(name) {
return name
.split(' ')
.map((part) => part[0])
.join('');
}

export default class Roster extends Component {
<template>
{{#each @people as |person|}}
<span class="avatar">{{initials person.name}}</span>
{{/each}}
</template>
}
```

`initials` doesn't read tracked state itself, but it participates in the graph all the same: it's re-evaluated for a person whenever the `name` passed to it is invalidated. Pure functions like this—parameterized derivations—are the most reusable form of derived state. See [Helper Functions](../../../components/helper-functions/) for more.

For derived state that several components need, the same composition rule applies one level up: put the root state _and_ its derivations together in a class (as in the `Cart` example in [Root State](../root-state/#toc_keep-root-state-private-expose-meaning)) or a [service](../../../services/), and let components consume the finished getters.

## Thinking in Derivations

When a new piece of UI state shows up, the order to try is:

1. **Can it be an expression in the template?** `{{if @isAdmin "superuser"}}` needs no JavaScript at all.
2. **Can it be a getter or pure function?** This covers nearly everything else.
3. **Is it genuinely new information that arrives from outside?** Only then is it [root state](../root-state/).

A symptom worth watching for: an event handler that updates several tracked properties "to keep them consistent" is almost always storing derivations. Move the consistency into getters, and let the handler write the one fact that actually changed:

```js
// 🛑 Don't: the handler maintains derived state by hand
selectPlan = (plan) => {
this.selectedPlan = plan;
this.price = plan.monthlyPrice * (this.isAnnual ? 12 : 1);
this.discount = this.isAnnual ? plan.annualDiscount : 0;
this.total = this.price - this.discount;
};

// ✅ Do: the handler records one fact; formulas do the rest
selectPlan = (plan) => {
this.selectedPlan = plan;
};

get price() {
return this.selectedPlan.monthlyPrice * (this.isAnnual ? 12 : 1);
}

get discount() {
return this.isAnnual ? this.selectedPlan.annualDiscount : 0;
}

get total() {
return this.price - this.discount;
}
```

In the first version, `total` is only correct if every code path that touches any input remembers to recompute it. In the second, `total` _cannot_ be wrong—toggling `isAnnual` from a completely different part of the app updates it automatically, through code that was written without any knowledge of that future feature. That's the payoff of the formula layer, and it's why "derive, don't sync" is the central habit of reactive programming.
116 changes: 116 additions & 0 deletions guides/release/in-depth-topics/reactivity/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
Reactivity is the heart of every modern UI framework: when your data changes, everything computed from that data—including the page itself—updates automatically. You declare _what_ the output should be for any given state, and the system figures out _when_ and _what_ to update.

You have been using Ember's reactivity system—called _autotracking_—since your first `@tracked` property. This section of the guides goes deeper: not just _how_ to use the APIs, but how to _think_ about reactivity, so that you can design state that stays correct as your application grows. The ideas here are not unique to Ember—systems like [Solid](https://www.solidjs.com/), [Starbeam](https://starbeamjs.com/), [Signalium](https://signalium.dev/), and the [TC39 Signals proposal](https://github.com/tc39/proposal-signals) share the same foundations—so learning them will sharpen how you think about UI state in general.

## The Spreadsheet Mental Model

The oldest and best mental model for reactivity is a spreadsheet.

In a spreadsheet, some cells contain plain _values_ that you type in: `A1 = 5`, `A2 = 10`. Other cells contain _formulas_ that reference those values: `A3 = A1 + A2`. When you change `A1`, you don't tell `A3` to update—it just does. And a formula can reference other formulas, building up a whole graph of computation that stays consistent no matter which value you edit.

Every reactive system is this spreadsheet, generalized:

- **Root state** is the cells you type into: the values that change directly, because a user clicked something, a server responded, or time passed. In Ember, root state is what you mark with `@tracked`.
- **Derived state** is the formulas: values computed _from_ root state (or from other derived state). In Ember, derived state is ordinary getters, functions, and template expressions.
- **Outputs** are where the data universe meets the outside world: the rendered DOM, the document title, a chart drawn on a canvas. In Ember, the primary output is your templates—the renderer watches everything your templates read, and updates the DOM when any of it changes.

Data flows in one direction: root state at the bottom, derivations stacked on top, outputs at the edge. Events from the outside world (clicks, responses, timers) write to root state, and the whole graph above stays consistent automatically.

```gjs
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { on } from '@ember/modifier';
import { fn } from '@ember/helper';

export default class Cart extends Component {
// Root state: the values that change directly
@tracked items = [];

// Derived state: formulas over root state
get subtotal() {
return this.items.reduce((sum, item) => sum + item.price, 0);
}

get tax() {
return this.subtotal * 0.08;
}

get total() {
return this.subtotal + this.tax;
}

addItem = (item) => {
// Events write to root state; everything else updates on its own
this.items = [...this.items, item];
};

// Output: the rendered page
<template>
<p>Total: {{this.total}}</p>
<button type="button" {{on "click" (fn this.addItem @product)}}>
Add to cart
</button>
</template>
}
```

Notice the proportions: one tracked property, three getters. This is typical of well-designed reactive code, and it's the most important habit this section of the guides hopes to teach: **most of your state should be derived, and only the irreducible minimum should be root state.** The [Root State](./root-state/) and [Derived State](./derived-state/) chapters develop this in detail.

## The Two Fundamental Operations

Underneath every reactive system—autotracking included—are just two operations:

- **Consume**: when a value is _read_ while something reactive is being computed (a template rendering, a cached getter evaluating), the system records "this computation used that value."
- **Invalidate** (or _dirty_): when a value is _written_, the system marks every computation that consumed it as out of date.

That's the whole trick. When Ember renders `{{this.total}}`, it evaluates `total`, which reads `subtotal` and `tax`, which read `items`—and because `items` is tracked, that read is _consumed_. Later, when `addItem` assigns to `this.items`, the write _invalidates_ the rendered output, and Ember schedules a re-render of exactly the parts of the DOM that consumed it.

Two properties of this design are worth internalizing:

**Dependencies are discovered at runtime, every time.** You never declare what a getter depends on; the system records what it _actually reads_ during each evaluation. This means even conditional dependencies just work:

```js
get displayName() {
return this.useNickname ? this.nickname : this.fullName;
}
```

While `useNickname` is `false`, changes to `nickname` don't invalidate anything—`displayName` never read it. If `useNickname` becomes `true`, the next evaluation reads `nickname`, and from then on changes to it propagate. The dependency graph rewires itself on every run.

**Tracking is synchronous.** The system can only observe reads that happen _during_ a reactive computation. If you read tracked state in a callback that runs later—after an `await`, inside a `setTimeout`—that read happens outside any tracking context, and nothing is consumed. This is rarely a problem in practice (templates, getters, and helpers are all synchronous), but it explains a class of "why didn't this update?" bugs. The [Reactivity and the Outside World](./outside-world/) chapter covers the boundary in detail.

## Pull, Not Push

There are two ways a reactive system can respond to a write:

- A **push**-based system eagerly re-runs every affected computation the moment a value changes.
- A **pull**-based (or _lazy_) system merely marks affected computations as out of date, and recomputes them only when someone actually needs their result.

Autotracking is pull-based. When you write to a tracked property, _no user code runs_. Your getters are not re-evaluated; nothing is recomputed. The write just bumps an internal revision counter and lets the renderer know that something it consumed is out of date. Later—asynchronously, but before the browser paints—the renderer re-evaluates the expressions in your templates and updates the DOM.

This has practical consequences that are easy to feel but hard to place if you don't know the model:

- **Writes are cheap, and they coalesce.** Setting ten tracked properties in one event handler causes one re-render, not ten. You don't need to batch updates yourself.
- **Unused state is free.** A derived value that nothing currently reads is never computed, no matter how often its inputs change. Work scales with what's _on the page_, not with what's _in your data_.
- **Reading state never observes a half-applied update.** Because derivations run on demand rather than in a notification cascade, there is no window where `tax` has updated but `subtotal` hasn't. Your data is always internally consistent.
- **There is no "re-run this code when X changes" primitive.** In a push-based system you might reach for an _effect_ for that. Ember deliberately doesn't offer one; the [Reactivity and the Outside World](./outside-world/) chapter explains why, and what to do instead.

## The Same Ideas, Elsewhere

If you've used other reactive systems—or read about "signals," which is what the broader JavaScript ecosystem calls these ideas—here is how the vocabulary maps:

| Concept | Ember | Solid | Starbeam | Signalium |
| ------------- | ----------------------------- | ---------------------- | ----------------- | -------------------- |
| Root state | `@tracked` | `createSignal` | cells, `reactive` | `signal` |
| Derived state | getters, `@cached` | functions, `createMemo` | formulas, getters | `reactive` functions |
| Outputs | templates, modifiers | JSX, `createEffect` | renderer, resources | watchers, relays |

The differences between these systems are mostly at the edges—when computation happens (Solid's effects are eager; Ember and Signalium are lazy) and how outputs are expressed (Solid hands you `createEffect`; Ember and Starbeam route effects through the renderer and lifecycle-managed constructs). The core—consume on read, invalidate on write, derive everything you can—is the same everywhere.

The rest of this section works through each layer of the model:

- [Root State](./root-state/) — what should (and should not) be root state, and how to design it.
- [Derived State](./derived-state/) — formulas: laziness, purity, caching, and composition.
- [Reactivity and the Outside World](./outside-world/) — outputs, side effects, async, and the edges of the graph.

For the mechanics of `@tracked` itself—updating, custom classes, arrays and objects, `@cached`—see [Autotracking In-Depth](../autotracking-in-depth/).
Loading
Loading