@@ -363,17 +360,16 @@ time.
When a new piece of UI state shows up, try these options in order:
-1. **Can it be an expression in the template?**
+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/).
+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
+A symptom to watch 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:
+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
@@ -403,7 +399,7 @@ get total() {
```
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.
+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 derived state, and it's why "derive,
diff --git a/guides/release/in-depth-topics/reactivity/index.md b/guides/release/in-depth-topics/reactivity/index.md
index e06484e93e..0cefee2916 100644
--- a/guides/release/in-depth-topics/reactivity/index.md
+++ b/guides/release/in-depth-topics/reactivity/index.md
@@ -5,7 +5,7 @@ state, and the framework figures out when and what to update.
You have been using Ember's reactivity system, called _autotracking_, since
your first `@tracked` property. The guides in this section go deeper than the
-API: they cover how to _think_ about reactivity, so that you can design state
+API: they cover how to think about reactivity, so that you can design state
that stays correct as your application grows. These ideas are not unique to
Ember - the broader JavaScript ecosystem calls them _signals_, and most modern
frameworks are built on the same foundations - so learning them will help you
@@ -15,15 +15,15 @@ reason about UI state in any framework.
Every reactive application is built from three layers:
-- **Root state** is the values that change directly, because a user clicked
+- _Root state_ is 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` or store in a tracked collection such as
`trackedArray`.
-- **Derived state** is the values computed _from_ root state, or from other
+- _Derived state_ is the values computed from root state, or from other
derived state. When you change a piece of root state, you don't tell the
derived values to update - they just do. In Ember, derived state is
ordinary getters, functions, and template expressions.
-- **Outputs** are where your data meets the outside world: the rendered DOM,
+- _Outputs_ are where your data 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.
@@ -73,37 +73,34 @@ export default class Cart extends Component {
}
```
-Notice the proportions in this component: one piece of root state, three
-getters.
-This is typical of well-designed reactive code, and it is the most important
-habit this section hopes to teach: **most of your state should be derived, and
-only the irreducible minimum should be root state.** The
+Notice that this component has one piece of root state and three getters.
+This is typical of well-designed reactive code, and it points at the most
+important habit these guides hope 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/) guides
develop this idea 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 that the computation used that value.
-- **Invalidate** (or _dirty_): when a value is _written_, the system marks
- every computation that consumed it as out of date.
+operations. When a value is read while something reactive is being computed
+(a template rendering, a cached getter evaluating), the system records that
+the computation used that value. This is called _consuming_. When a value is
+written, the system marks every computation that consumed it as out of date.
+This is called _invalidating_, or _dirtying_.
That's the whole trick! When Ember renders `{{this.total}}` in the component
above, it evaluates `total`, which reads `subtotal` and `tax`, which read
`items` - and because `items` is a tracked collection, those reads are
-_consumed_. Later, when `addItem` pushes into `this.items`, the write
-_invalidates_ the rendered output, and Ember schedules a rerender of exactly
+consumed. Later, when `addItem` pushes into `this.items`, the write
+invalidates the rendered output, and Ember schedules a rerender of exactly
the parts of the DOM that consumed it.
-Two properties of this design are worth internalizing.
+Two properties of this design come up again and again.
-First, **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:
+First, 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() {
@@ -116,45 +113,43 @@ anything, because `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.
-Second, **tracking is synchronous.** The system can only observe reads that
-happen _while_ a reactive computation is running. If you read tracked state in
-a callback that runs later - after an `await`, or inside a `setTimeout` - that
-read happens outside any tracking context, and nothing is consumed. This is
-rarely a problem in practice, since templates, getters, and helpers are all
-synchronous, but it explains a whole class of "why didn't this update?" bugs.
-The [Reactivity and the Outside World](./outside-world/) guide covers this
-boundary in detail.
+Second, tracking is synchronous. The system can only observe reads that
+happen while a reactive computation is running. If you read tracked state in
+a callback that runs later - after an `await`, or inside a `setTimeout` -
+that read happens outside any tracking context, and nothing is consumed. This
+is rarely a problem in practice, since templates, getters, and helpers are
+all synchronous, but it explains a whole class of "why didn't this update?"
+bugs. The [Reactivity and the Outside World](./outside-world/) guide covers
+this 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.
+- 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 lets the renderer know that something it consumed is out of date.
-Later - asynchronously, but before the browser paints - the renderer
+Autotracking is pull-based. When you write to a tracked property, no user
+code runs at all. Your getters are not re-evaluated; nothing is recomputed.
+The write just 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:
+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 rerender, not ten. You don't need to batch
+- Writes are cheap, and they coalesce. Setting ten tracked properties in one
+ event handler causes one rerender, not ten, so 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
+- 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.
+- 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/) guide
explains why, and what to do instead.
diff --git a/guides/release/in-depth-topics/reactivity/outside-world.md b/guides/release/in-depth-topics/reactivity/outside-world.md
index b3074be4a5..577ced0458 100644
--- a/guides/release/in-depth-topics/reactivity/outside-world.md
+++ b/guides/release/in-depth-topics/reactivity/outside-world.md
@@ -1,16 +1,17 @@
The reactive graph - [root state](../root-state/) at the bottom,
-[derived state](../derived-state/) above it - is a closed, pure world: values
-in, values out, no surprises. But applications exist to affect the world
-outside that graph: paint pixels, play audio, talk to servers, listen to
-sockets. This guide is about the edges of the graph - how data gets in, how it
-gets out, and why Ember draws those edges where it does.
+[derived state](../derived-state/) above it - is deliberately
+self-contained: values in, values out, no side effects. But applications
+exist to affect the world outside that graph: paint pixels, play audio, talk
+to servers, listen to sockets. This guide is about the edges of the graph -
+how data gets in, how it gets out, and why Ember draws those edges where it
+does.
The shape to keep in mind:
-- **Inputs** write to root state: event handlers, response callbacks,
+- Inputs write to root state: event handlers, response callbacks,
subscription messages, timers.
-- **Outputs** read the graph and act on the world. In Ember, the output is
- the renderer; other side effects are _managed effects_, covered below.
+- Outputs read the graph and act on the world. In Ember, the output is the
+ renderer; other side effects are managed effects, covered below.
Everything in between stays pure.
@@ -21,8 +22,8 @@ reactive systems - and work back around to inputs.
In some reactive systems, you wire outputs yourself with an _effect_
primitive: a function that re-runs whenever the reactive values it read
-change. If you ask "where is Ember's effect primitive?", the first answer is:
-**you've been using it all along - it's the renderer.**
+change. If you ask "where is Ember's effect primitive?", the first answer is
+that you've been using it all along - it's the renderer.
A template is a declaration of effects: every `{{expression}}` and every
attribute binding is a tiny "when this value changes, update that DOM" rule.
@@ -37,36 +38,36 @@ changed.
The second answer is that the omission is deliberate, and the reasons are
instructive:
-- **Effects are eager.** An effect must re-run on every change to its inputs,
+- Effects are eager. An effect must re-run on every change to its inputs,
whether or not anyone needs the result, breaking the lazy, pull-based
economics that make the rest of the system cheap.
-- **Effect ordering is undefined.** When one change triggers several effects,
- the execution order is unspecified. Correctness that depends on effect
- order is a latent bug.
-- **Effects that write state are a trap.** The most common effect mistake is
- using one to _sync_ state: "when X changes, update Y." Now Y is stale until
+- Effect ordering is undefined. When one change triggers several effects, the
+ execution order is unspecified. Correctness that depends on effect order is
+ a latent bug.
+- Effects that write state are a trap. The most common effect mistake is
+ using one to sync state: "when X changes, update Y." Now Y is stale until
the effect runs, can disagree with X, and if the effect's write triggers
another effect, you have a cascade or an infinite loop. The answer is to
- _derive, don't sync_:
- in Ember, [the getter was the answer all along](../derived-state/), and the
+ derive instead of syncing: in Ember,
+ [the getter was the answer all along](../derived-state/), and the
backtracking assertion makes write-during-derivation a loud error rather
than a quiet bug.
-- **Almost every "effect" is something more specific.** Look closely at real
- effect code and you find: derived values (should be getters), DOM
- manipulation (should be scoped to an element), and lifecycle-bound processes
- like subscriptions (should be tied to an owner's lifetime, with cleanup).
- Ember provides each of those as a dedicated, managed construct instead of
- one general escape hatch.
+- Almost every "effect" is something more specific. Look closely at real
+ effect code and you find derived values (should be getters), DOM
+ manipulation (should be scoped to an element), and lifecycle-bound
+ processes like subscriptions (should be tied to an owner's lifetime, with
+ cleanup). Ember provides each of those as a dedicated, managed construct
+ instead of one general escape hatch.
## Managed Effects: Attached to a Lifetime
-Rendering is the only true _output_ in Ember - but it isn't the only side
-effect. When you do need to act on the world yourself, Ember's tools all share
-one design: the effect is attached to something with a _lifetime_, runs with
-cleanup, and re-runs through the same autotracking as everything else.
+Rendering is the only true output in Ember - but it isn't the only side
+effect. When you do need to act on the world yourself, Ember's tools all
+share one design: the effect is attached to something with a lifetime, runs
+with cleanup, and re-runs through the same autotracking as everything else.
-**Modifiers** are effects scoped to a DOM element. They run when the element
-is rendered, re-run (after cleanup) when tracked state they consumed changes,
+Modifiers are effects scoped to a DOM element. They run when the element is
+rendered, re-run (after cleanup) when tracked state they consumed changes,
and clean up when the element goes away:
```js {data-filename=app/modifiers/draw-chart.js}
@@ -93,7 +94,7 @@ cannot run before there's an element, cannot leak after the element is gone,
and declares in the template exactly where its impact lands. See
[Template Lifecycle, DOM, and Modifiers](../../../components/template-lifecycle-dom-and-modifiers/).
-**Destroyables** cover lifecycle-bound work with no element. Anything with an
+Destroyables cover lifecycle-bound work with no element. Anything with an
owner - components, services, helpers - can pair setup with guaranteed
teardown via `registerDestructor` from
[`@ember/destroyable`](https://api.emberjs.com/ember/release/modules/@ember%2Fdestroyable):
@@ -119,9 +120,9 @@ export default class ClockService extends Service {
```
Any getter, anywhere in the app, can now derive from `clock.now` - "seconds
-remaining," "is the store open," a formatted timestamp - and every one of them
-updates each second, while the actual side effect (one interval, one cleanup)
-stays in one place.
+remaining," "is the store open," a formatted timestamp - and every one of
+them updates each second, while the actual side effect (one interval, one
+cleanup) stays in one place.
This setup-plus-cleanup-plus-reactive-state package is commonly called a
_resource_. In the Ember ecosystem, the
@@ -143,23 +144,23 @@ this.socket.addEventListener('message', (event) => {
```
Writes from the outside are always safe. The backtracking assertion only
-restricts writes _during_ a reactive computation (inside getters and
-templates). An event callback runs outside any computation, so it can write as
-much as it likes, and all the writes coalesce into a single rerender.
+restricts writes during a reactive computation (inside getters and
+templates). An event callback runs outside any computation, so it can write
+as much as it likes, and all the writes coalesce into a single rerender.
The clock service above is the full input pattern in miniature: an external
process (the interval) feeds the graph through one tracked write, and cleanup
-is bound to a lifetime. Subscriptions, `ResizeObserver`s, `BroadcastChannel`s -
-they all take this shape: **subscribe with cleanup; on each notification,
-write root state; derive everything else.**
+is bound to a lifetime. Subscriptions, `ResizeObserver`s,
+`BroadcastChannel`s - they all take the same shape. Subscribe with cleanup,
+write root state on each notification, and derive everything else.
## Async: Tracking Stops at `await`
-Tracking contexts are synchronous. The system records reads that happen
-_while_ a template expression or cached getter is computing - and a
-computation, in JavaScript, ends at the first `await`. Code after an `await`
-(or inside `setTimeout`, or a `.then()` callback) runs later, outside the
-computation that started it, so nothing it reads is consumed:
+Tracking contexts are synchronous. The system records reads that happen while
+a template expression or cached getter is computing - and a computation, in
+JavaScript, ends at the first `await`. Code after an `await` (or inside
+`setTimeout`, or a `.then()` callback) runs later, outside the computation
+that started it, so nothing it reads is consumed:
```js
// 🛑 The renderer cannot see through this
@@ -168,10 +169,10 @@ get userName() {
}
```
-Derivations must be synchronous. The reactive way to handle async work follows
-from the input rule above: the _request_ is a side effect; its _progress_ is
-root state. Run the effect at a lifetime boundary, and write each phase of it
-into tracked properties:
+Derivations must be synchronous. The reactive way to handle async work
+follows from the input rule above: the request is a side effect, and its
+progress is root state. Run the effect at a lifetime boundary, and write each
+phase of it into tracked properties:
```gjs {data-filename=app/components/profile.gjs}
import Component from '@glimmer/component';
@@ -217,24 +218,23 @@ export default class Profile extends Component {
}
```
-Once async state is _data_, it stops being a special case: "show a spinner
+Once async state is data, it stops being a special case: "show a spinner
while pending" is just another derivation, the same `{{#if}}` as anything
else. This "reactive promise" shape - status, value, and error as reactive
fields - is available ready-made from libraries like
[ember-resources](https://github.com/NullVoxPopuli/ember-resources) and
-[WarpDrive](https://docs.warp-drive.io/)'s request state - or in a dozen lines
-of your own, as above.
+[WarpDrive](https://docs.warp-drive.io/)'s request state - or in a dozen
+lines of your own, as above.
Note one limitation of the example as written: the request is created in a
field initializer, so it captures `userId` once and won't re-fetch if the
argument changes - the construction-time snapshot trap described in
[Deferring Consumption](../derived-state/#toc_deferring-consumption).
-Re-running an effect when its reactive inputs change is
-exactly the job of the managed constructs from earlier - a modifier (if
-there's a sensible element) or a resource. That's the general rule of this
-guide closing the loop: **when an effect needs to respond to the graph, give
-it a lifetime the framework manages; when the world needs to update the graph,
-write root state.**
+Re-running an effect when its reactive inputs change is exactly the job of
+the managed constructs from earlier - a modifier (if there's a sensible
+element) or a resource. When an effect needs to respond to the graph, give it
+a lifetime the framework manages; when the world needs to update the graph,
+write root state.
## Choosing the Right Edge
diff --git a/guides/release/in-depth-topics/reactivity/root-state.md b/guides/release/in-depth-topics/reactivity/root-state.md
index 928d107f3c..8e54ee6c93 100644
--- a/guides/release/in-depth-topics/reactivity/root-state.md
+++ b/guides/release/in-depth-topics/reactivity/root-state.md
@@ -1,5 +1,5 @@
Root state is the foundation of the reactive graph: the values that change
-_directly_, rather than being computed from something else. Everything else in
+directly, rather than being computed from something else. Everything else in
your application - derived values, rendered DOM - is a consequence of root
state. That makes designing your root state the highest-leverage decision you
make when managing UI state. Get it right, and the rest of your code becomes
@@ -16,21 +16,20 @@ class Draft {
}
```
-A write to a tracked property is the _only_ way anything changes in a reactive
-application. Every update you see on screen traces back to some event handler,
-timer, or response callback assigning to root state.
+A write to a tracked property is the only way anything changes in a reactive
+application. Every update you see on screen traces back to some event
+handler, timer, or response callback assigning to root state.
## What Qualifies as Root State
-A value belongs in root state only if _both_ of these are true:
+A value belongs in root state only if both of these are true:
-1. It changes over time, in response to the outside world (user input, network
- responses, timers).
+1. It changes over time, in response to the outside world (user input,
+ network responses, timers).
2. It cannot be computed from other state.
-The second rule is the one that gets broken most often in practice, and it's
-worth being strict about. For every tracked property, ask yourself: _could I
-compute this instead?_
+The second rule is the one that gets broken most often in practice. For every
+tracked property, ask yourself: _could I compute this instead?_
```js
class Cart {
@@ -48,8 +47,9 @@ class Cart {
The tracked `itemCount` looks harmless, but it creates a second source of
truth. Every code path that changes `items` must now also remember to update
-`itemCount`, forever. Once two copies of the truth exist, they will eventually
-disagree - and that whole class of bug simply doesn't exist for the getter.
+`itemCount`, forever. Once two copies of the truth exist, they will
+eventually disagree - and that whole class of bug simply doesn't exist for
+the getter.
Storing derived values is sometimes pitched as an optimization. However,
autotracking already recomputes lazily, and only when inputs change, so the
@@ -57,16 +57,15 @@ optimization is usually imaginary. When a derivation really is expensive, you
can [cache it](../derived-state/#toc_caching) instead of promoting it to root
state.
-A useful instinct: a well-factored reactive
-application has surprisingly _little_ root state. A search page might have
-exactly two root values - the query string and the raw results - while
-everything else on screen (filtered lists, counts, empty-state flags, disabled
-buttons) is derived.
+As a rule of thumb, a well-factored reactive application has surprisingly
+little root state. A search page might have exactly two root values - the
+query string and the raw results - while everything else on screen (filtered
+lists, counts, empty-state flags, disabled buttons) is derived.
## Root State Is Not a Cache for Someone Else's Truth
-A special case of "could I compute this instead?" arises with _data that
-arrives from elsewhere_: arguments passed to your component, records from your
+A special case of "could I compute this instead?" arises with data that
+arrives from elsewhere: arguments passed to your component, records from your
data layer, the current route. The reactive system already tracks these.
Copying them into your own tracked properties creates the synchronization
problem all over again:
@@ -91,24 +90,23 @@ class UserCard extends Component {
```
If you genuinely need "the argument, until the user edits it locally" (a form
-draft, for example), that's _new_ root state whose initial value happens to
-come from elsewhere. Create it explicitly in response to a user action, not by
-mirroring the argument on every change. The
+draft, for example), that's new root state whose initial value happens to
+come from elsewhere. Create it explicitly in response to a user action, not
+by mirroring the argument on every change. The
[Patterns for Components](../../patterns-for-components/) guide shows several
shapes of this.
## Writes, Equality, and Dirtying
-When does a write actually invalidate things? By default, the answer is
-simple: _every_ assignment to a tracked property dirties it, even if you
-assign the value it already had.
+When does a write actually invalidate things? By default, every assignment to
+a tracked property dirties it, even if you assign the value it already had.
```js
this.count = this.count; // still invalidates everything that consumed `count`
```
-Consumers re-evaluate, and the renderer re-checks the DOM it produced. The DOM
-itself won't change if the final values are equal, but the recomputation
+Consumers re-evaluate, and the renderer re-checks the DOM it produced. The
+DOM itself won't change if the final values are equal, but the recomputation
happens.
When a write that changes nothing shouldn't dirty anything, the decorator
@@ -130,38 +128,39 @@ class Player {
Without options, `@tracked` keeps its historical always-dirty behavior, so
existing code is unaffected.
-When is custom equality worth it? The default is fine for most state - a
-write usually happens because something actually changed. Custom equality
-pays off when writes frequently _don't_ change the value:
-
-- **High-frequency events that usually land on the same value.** Writing the
- current scroll direction (`'up'` or `'down'`) on every scroll event, or the
- current breakpoint name on every resize: the event fires constantly, but
- the value only changes at the moment of reversal or crossing.
-- **Data that is re-fetched but rarely different.** Polling a server returns
- a fresh object every time. By reference it is always "new"; by content it
- is almost always the same as last time.
-- **Value-like objects.** Dates, durations, coordinates: two distinct
- instances can represent the same value. Comparing by content - for example,
- `(a, b) => a.getTime() === b.getTime()` for dates - reflects what the value
- _means_ rather than where it lives in memory.
+Most state doesn't need custom equality, because a write usually happens
+when something actually changed. It pays off when writes frequently don't
+change the value:
+
+- High-frequency events that usually land on the same value. For instance,
+ writing the current scroll direction (`'up'` or `'down'`) on every scroll
+ event, or the current breakpoint name on every resize: the event fires
+ constantly, but the value only changes at the moment of reversal or
+ crossing.
+- Data that is re-fetched but rarely different. Polling a server returns a
+ fresh object every time. By reference it is always "new"; by content it is
+ almost always the same as last time.
+- Value-like objects such as dates, durations, and coordinates, where two
+ distinct instances can represent the same value. Comparing by content - for
+ example, `(a, b) => a.getTime() === b.getTime()` for dates - reflects what
+ the value means rather than where it lives in memory.
In each of these cases, without an equality check every write invalidates
every consumer, and the renderer re-evaluates everything downstream just to
conclude that nothing changed. An equality check cuts that work off at the
-root. The flip side: the check itself runs on every write, so for values that
-really do change on most writes, the default is the cheaper choice.
+root. On the other hand, the check itself runs on every write, so for values
+that really do change on most writes, the default is the cheaper choice.
-Because dirtying is per-property, the _granularity_ of your root state
+Because dirtying is per-property, the granularity of your root state
determines the granularity of updates. Three tracked properties invalidate
independently; one tracked object replaced wholesale invalidates everything
that read any part of it. Reactive systems thrive on fine-grained changes:
prefer shapes that let you write exactly the piece that changed, and keep the
-_identity_ of everything else stable.
+identity of everything else stable.
## Mutable Data: Track the Collection
-`@tracked` tracks _assignments to the property_, not mutations inside the
+`@tracked` tracks assignments to the property, not mutations inside the
value. Pushing into a plain array or setting a key on a plain object is
invisible to the system:
@@ -177,7 +176,7 @@ class ShoppingList {
The right tool here is a tracked collection from
[`@ember/reactive/collections`](https://api.emberjs.com/ember/release/modules/@ember%2Freactive%2Fcollections),
-which tracks reads and writes of its _contents_ at fine granularity:
+which tracks reads and writes of its contents at fine granularity:
```js
import { trackedArray } from '@ember/reactive/collections';
@@ -193,16 +192,16 @@ class ShoppingList {
Note that the property itself no longer needs `@tracked`. The collection
carries its own reactivity, and the property is never reassigned. Two details
-worth knowing: the collection functions _copy_ the data you pass in, so
-mutating the tracked collection never mutates the original; and tracked
-collections are shallow - `trackedObject`'s properties are tracked, but
-objects stored _inside_ it are ordinary objects unless you wrap them too. See
+to know: the collection functions copy the data you pass in, so mutating the
+tracked collection never mutates the original; and tracked collections are
+shallow - `trackedObject`'s properties are tracked, but objects stored inside
+it are ordinary objects unless you wrap them too. See
[Autotracking In-Depth](../../autotracking-in-depth/#toc_plain-old-javascript-objects-pojos)
for the full tour of `trackedObject`, `trackedArray`, `trackedMap`, and
`trackedSet`.
-You may also see code that _replaces_ the value instead, assigning a
-brand-new array to the tracked property:
+You may also see code that replaces the value instead, assigning a brand-new
+array to the tracked property:
```js
addItem = (item) => {
@@ -214,16 +213,16 @@ The assignment is a tracked write, so this updates - but it is the coarsest
change there is. Every consumer of `items` invalidates, even ones that only
cared about one entry, and the array's identity changes on every update,
which defeats downstream `===` checks and caches (see
-[stable identity](../derived-state/#toc_caching)). "One item changed" becomes
-"everything changed." Retain identity wherever possible: keep one long-lived
-tracked collection and mutate it, so the system invalidates only what
-actually changed. Replacement is best reserved for genuinely value-like data
-- a string, a date, a small tuple - where the new value simply _is_ a
-different value.
+[stable identity](../derived-state/#toc_caching)). Something that was really
+a one-item change gets treated as if the entire array were new. Retain
+identity wherever possible: keep one long-lived tracked collection and mutate
+it, so that the system invalidates only what actually changed. Replacement is
+best reserved for genuinely value-like data - a string, a date, a small
+tuple - where the new value simply is a different value.
## Keep Root State Private, Expose Meaning
-Root state is an implementation detail. The code that _uses_ your state
+Root state is an implementation detail. The code that uses your state
shouldn't know (or care) which parts are stored and which are computed. A
good pattern is to keep reactive storage private and expose a domain-shaped
public API:
@@ -235,7 +234,7 @@ export class Cart {
// Root state: private, mutable, reactive
#items = trackedMap();
- // Public API: domain-shaped, read-only, derived
+ // Public API: read-only, derived
get items() {
return Array.from(this.#items.values());
}
@@ -248,7 +247,7 @@ export class Cart {
return this.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}
- // Mutations: named after what they mean, not how they're stored
+ // Mutations, named for what they do to the cart
add(product, quantity = 1) {
this.#items.set(product.id, { ...product, quantity });
}
@@ -262,25 +261,22 @@ export class Cart {
Consumers read `cart.total` and call `cart.add(product)` - ordinary
JavaScript, fully reactive, with no way to corrupt the internal storage. If
you later change how items are stored, nothing outside the class notices.
-Classes like this need no framework machinery at all; they work in components,
-services, route models, and plain unit tests alike.
+Classes like this need no framework machinery at all; they work in
+components, services, route models, and plain unit tests alike.
## Classes Are Root State, Too
Step back and look at what `Cart` is: tracked storage plus the getters
-derived from it, bundled behind one reference. From the outside, an _instance_
+derived from it, bundled behind one reference. From the outside, an instance
of `Cart` is a single reactive value - root state that happens to be
non-primitive. A tracked property can hold a number or a string; it can just
as well hold a `Cart`.
That gives you two levels of granularity, and the dirtying rule from earlier
-applies to both:
-
-- **Mutate the instance** - call `cart.add(product)` - and only consumers of
- the affected internal state invalidate.
-- **Replace the instance** - assign a new `Cart` to a tracked property - and
- everything that read any part of it invalidates. That's a "reset all" in a
- single write:
+applies to both. Mutating the instance (calling `cart.add(product)`)
+invalidates only the consumers of the affected internal state. Replacing the
+instance (assigning a new `Cart` to a tracked property) invalidates
+everything that read any part of it - a single write that resets everything:
```js
import Component from '@glimmer/component';
@@ -350,7 +346,7 @@ pattern - effects tied to a lifetime - in depth.
One more design rule for state classes: when a class needs to read state that
lives somewhere else (component arguments, another class's tracked fields),
-have it accept _functions_ rather than values, so that it reads the current
+have it accept functions rather than values, so that it reads the current
state on every use instead of a snapshot from construction time. See
[Deferring Consumption](../derived-state/#toc_deferring-consumption) for the
full pattern.
@@ -371,11 +367,11 @@ count.value = 1; // writing invalidates consumers
```
Unlike the decorator, a standalone value checks equality by default (using
-`Object.is`), so assigning the value it already holds invalidates nothing. You
-can pass your own comparison with `tracked(initial, { equals })` - useful in
-exactly the situations described in
-[Writes, Equality, and Dirtying](#toc_writes-equality-and-dirtying) above. For
-example, a poll that returns a fresh object every time:
+`Object.is`), so assigning the value it already holds invalidates nothing.
+You can pass your own comparison with `tracked(initial, { equals })`, which
+is useful in exactly the situations described in
+[Writes, Equality, and Dirtying](#toc_writes-equality-and-dirtying) above.
+For example, a poll that returns a fresh object every time:
```js
import { tracked } from '@glimmer/tracking';
@@ -395,8 +391,8 @@ serverStatus.value = await fetchStatus();
```
Beyond `.value` there are function shorthands: `get()` and `set(value)`, plus
-`update((current) => next)` - which writes based on the current value
-_without_ consuming it - and `freeze()`, which prevents all further writes.
+`update((current) => next)` - which writes based on the current value without
+consuming it - and `freeze()`, which prevents all further writes.
For application code, classes with `@tracked` remain the primary tool. The
function form fills the gaps a decorator can't reach: function-based helpers
@@ -431,16 +427,16 @@ for historical compatibility).
Root state needs an owner - something whose lifetime matches the state's
lifetime:
-- **Component state** belongs on the component, or on plain classes the
- component creates. It's created and thrown away with the component instance.
- See [Component State and Actions](../../../components/component-state-and-actions/).
-- **Application-wide state** belongs in a [service](../../../services/), which
+- Component state belongs on the component, or on plain classes the component
+ creates. It's created and thrown away with the component instance. See
+ [Component State and Actions](../../../components/component-state-and-actions/).
+- Application-wide state belongs in a [service](../../../services/), which
lives as long as the application and can be injected anywhere.
-- **URL-driven state** (the current route, query params) belongs in the
- router. Reach for it via route models and query params rather than copying
- it into tracked properties.
+- URL-driven state (the current route, query params) belongs in the router.
+ Reach for it via route models and query params rather than copying it into
+ tracked properties.
-One place root state should generally _not_ live is module scope:
+One place root state should generally not live is module scope:
```js
// 🛑 Avoid in apps
From 67ae08b550ba7c06b05ffee6112a6ce4ed0204f6 Mon Sep 17 00:00:00 2001
From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com>
Date: Tue, 4 Aug 2026 11:02:42 -0400
Subject: [PATCH 7/8] Feedback
---
.../in-depth-topics/autotracking-in-depth.md | 2 +-
.../reactivity/derived-state.md | 10 ++---
.../in-depth-topics/reactivity/index.md | 6 +--
...outside-world.md => inputs-and-outputs.md} | 0
.../in-depth-topics/reactivity/root-state.md | 39 ++++++++++++++++---
guides/release/pages.yml | 4 +-
6 files changed, 45 insertions(+), 16 deletions(-)
rename guides/release/in-depth-topics/reactivity/{outside-world.md => inputs-and-outputs.md} (100%)
diff --git a/guides/release/in-depth-topics/autotracking-in-depth.md b/guides/release/in-depth-topics/autotracking-in-depth.md
index 2f68ad1e96..f40e2e99d1 100644
--- a/guides/release/in-depth-topics/autotracking-in-depth.md
+++ b/guides/release/in-depth-topics/autotracking-in-depth.md
@@ -199,7 +199,7 @@ This will also trigger a rerender. No matter where the update occurs, updating
a tracked property will let Ember know to rerender any affected portion of the
app. Writing to tracked state from callbacks like this is the standard way
data from the outside world enters Ember's reactivity system - see
-[Reactivity and the Outside World](../reactivity/outside-world/) for more on
+[Inputs and Outputs](../reactivity/inputs-and-outputs/) for more on
this pattern.
### Tracking Through Methods
diff --git a/guides/release/in-depth-topics/reactivity/derived-state.md b/guides/release/in-depth-topics/reactivity/derived-state.md
index 564974bc41..43fcb42d84 100644
--- a/guides/release/in-depth-topics/reactivity/derived-state.md
+++ b/guides/release/in-depth-topics/reactivity/derived-state.md
@@ -71,7 +71,7 @@ One consequence is that you should never rely on when, or whether, a getter
runs. 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/).
+state - see [Inputs and Outputs](../inputs-and-outputs/).
## Derivations Must Be Pure
@@ -86,10 +86,10 @@ 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. When you hit this error, restructure the
-code so the write isn't needed:
+This error is the _backtracking assertion_: render evaluates your derivations
+top to bottom, and a write partway through would invalidate output that was
+already produced. When you hit it, restructure the code so the write isn't
+needed:
```js
// 🛑 Don't: a "derivation" that pushes its result somewhere else
diff --git a/guides/release/in-depth-topics/reactivity/index.md b/guides/release/in-depth-topics/reactivity/index.md
index 0cefee2916..ed2ac48130 100644
--- a/guides/release/in-depth-topics/reactivity/index.md
+++ b/guides/release/in-depth-topics/reactivity/index.md
@@ -119,7 +119,7 @@ a callback that runs later - after an `await`, or inside a `setTimeout` -
that read happens outside any tracking context, and nothing is consumed. This
is rarely a problem in practice, since templates, getters, and helpers are
all synchronous, but it explains a whole class of "why didn't this update?"
-bugs. The [Reactivity and the Outside World](./outside-world/) guide covers
+bugs. The [Inputs and Outputs](./inputs-and-outputs/) guide covers
this boundary in detail.
## Pull, Not Push
@@ -151,7 +151,7 @@ you don't know the model:
`tax` has updated but `subtotal` hasn't.
- 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/) guide
+ offer one; the [Inputs and Outputs](./inputs-and-outputs/) guide
explains why, and what to do instead.
## Where to Go from Here
@@ -162,7 +162,7 @@ The rest of this section works through each layer of the model:
and how to design it.
- [Derived State](./derived-state/) - laziness, purity, caching, composition,
and deferring consumption.
-- [Reactivity and the Outside World](./outside-world/) - outputs, side
+- [Inputs and Outputs](./inputs-and-outputs/) - inputs, outputs, side
effects, async, and the edges of the graph.
For the mechanics of `@tracked` itself - updating, custom classes, arrays and
diff --git a/guides/release/in-depth-topics/reactivity/outside-world.md b/guides/release/in-depth-topics/reactivity/inputs-and-outputs.md
similarity index 100%
rename from guides/release/in-depth-topics/reactivity/outside-world.md
rename to guides/release/in-depth-topics/reactivity/inputs-and-outputs.md
diff --git a/guides/release/in-depth-topics/reactivity/root-state.md b/guides/release/in-depth-topics/reactivity/root-state.md
index 8e54ee6c93..aa046fd3d8 100644
--- a/guides/release/in-depth-topics/reactivity/root-state.md
+++ b/guides/release/in-depth-topics/reactivity/root-state.md
@@ -125,8 +125,19 @@ class Player {
}
```
-Without options, `@tracked` keeps its historical always-dirty behavior, so
-existing code is unaffected.
+Without options, `@tracked` has always-dirty behavior.
+
+
+
+
+
Zoey says...
+
+ @tracked({ equals }) and the tracked() function described later on this page are new in Ember 7.3.
+
+
+

+
+
Most state doesn't need custom equality, because a write usually happens
when something actually changed. It pays off when writes frequently don't
@@ -341,7 +352,7 @@ export default class Dashboard extends Component {
`registerDestructor` gives `Poller` its cleanup; `associateDestroyableChild`
links it to the component, so when the component is destroyed, the poller is
destroyed with it. The
-[Reactivity and the Outside World](../outside-world/) guide covers this
+[Inputs and Outputs](../inputs-and-outputs/) guide covers this
pattern - effects tied to a lifetime - in depth.
One more design rule for state classes: when a class needs to read state that
@@ -368,8 +379,26 @@ count.value = 1; // writing invalidates consumers
Unlike the decorator, a standalone value checks equality by default (using
`Object.is`), so assigning the value it already holds invalidates nothing.
-You can pass your own comparison with `tracked(initial, { equals })`, which
-is useful in exactly the situations described in
+This is the one place the two forms of `tracked` disagree, and it's easy to
+trip over. The same-looking write behaves differently in each:
+
+```js
+class Counter {
+ @tracked count = 0;
+}
+let counter = new Counter();
+counter.count = 0; // dirties: the decorator does not check equality
+
+const count = tracked(0);
+count.value = 0; // does nothing: the value is already 0
+```
+
+If you need the two forms to match, give the decorator an equality check
+(`@tracked({ equals: Object.is })`), or opt the standalone value out of
+checking with `equals: () => false`.
+
+You can also pass your own comparison with `tracked(initial, { equals })`,
+which is useful in exactly the situations described in
[Writes, Equality, and Dirtying](#toc_writes-equality-and-dirtying) above.
For example, a poll that returns a fresh object every time:
diff --git a/guides/release/pages.yml b/guides/release/pages.yml
index 41bb8722eb..d7d39c40de 100644
--- a/guides/release/pages.yml
+++ b/guides/release/pages.yml
@@ -161,8 +161,8 @@
url: "root-state"
- title: "Derived State"
url: "derived-state"
- - title: "Reactivity and the Outside World"
- url: "outside-world"
+ - title: "Inputs and Outputs"
+ url: "inputs-and-outputs"
- title: "Patterns for Components"
url: "patterns-for-components"
- title: "Patterns for Actions"
From ffd867a227eeba8afed39f2531803219f81a9d21 Mon Sep 17 00:00:00 2001
From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com>
Date: Sun, 9 Aug 2026 16:02:56 -0400
Subject: [PATCH 8/8] Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
---
guides/release/in-depth-topics/reactivity/root-state.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/guides/release/in-depth-topics/reactivity/root-state.md b/guides/release/in-depth-topics/reactivity/root-state.md
index aa046fd3d8..09d089aac8 100644
--- a/guides/release/in-depth-topics/reactivity/root-state.md
+++ b/guides/release/in-depth-topics/reactivity/root-state.md
@@ -187,7 +187,7 @@ class ShoppingList {
The right tool here is a tracked collection from
[`@ember/reactive/collections`](https://api.emberjs.com/ember/release/modules/@ember%2Freactive%2Fcollections),
-which tracks reads and writes of its contents at fine granularity:
+which tracks reads and writes of its contents:
```js
import { trackedArray } from '@ember/reactive/collections';