Skip to content

Overload cached for non-class use - #19

Merged
NullVoxPopuli merged 7 commits into
NullVoxPopuli:nvp/cached-overloadedfrom
NullVoxPopuli-ai-agent:overload-cached-for-non-class-use
Jul 31, 2026
Merged

Overload cached for non-class use#19
NullVoxPopuli merged 7 commits into
NullVoxPopuli:nvp/cached-overloadedfrom
NullVoxPopuli-ai-agent:overload-cached-for-non-class-use

Conversation

@NullVoxPopuli-ai-agent

@NullVoxPopuli-ai-agent NullVoxPopuli-ai-agent commented Jul 31, 2026

Copy link
Copy Markdown

Summary

Adds a new RFC proposing an overload for cached (from @glimmer/tracking) so it can be used outside of classes — the derived-state companion to RFC 1071 (overloaded tracked).

  • Copied from the latest RFC 1071 text on the emberjs remote and re-worded for cached throughout.
  • Re-uses the Reactive / ReadOnlyReactive interfaces defined in RFC 1071; cached(fn, options) returns a CachedValue extends ReadOnlyReactive (no set/update/freeze — it is born read-only).
  • options.equals (default Object.is) preserves referential identity when a re-computation produces an equivalent value; options.description for debugging.
  • Positions cached(fn) as the app-facing replacement for createCache/getValue from @glimmer/tracking/primitives/cache (RFC 615).

The file is named text/0000-… for now; it should be renumbered to the upstream PR number when proposed on emberjs/rfcs (RFC 1071 followed the same convention).

Implementation PR: emberjs/ember.js#21537

🤖 Generated with Claude Code

@bolt-new-by-stackblitz

Copy link
Copy Markdown

Review PR in StackBlitz Codeflow Run & review this pull request in StackBlitz Codeflow.


Our documentation / guides currently don't have much of anything on reactivity, and over the years, it's been useful to talk about reactive primitives as things _outside_ of classes, and compose/wrap them in to refactoring boundaries (classes, components, etc).

[RFC#1071](https://github.com/emberjs/rfcs/blob/master/text/1071-overload-tracked-for-non-class-use.md) gave us `tracked()` for _root state_ outside of classes, but there is no ergonomic equivalent for _derived state_ -- today, memoized derivation outside of a class requires either a class with a `@cached` getter, or dropping down to the memoization primitives from [RFC#615](https://github.com/emberjs/rfcs/blob/master/text/0615-autotracking-memoization.md).

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

derived state is not the same as cached state. do not mix these up

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reworded in 4a80599: derived state is plain functions and needs no API — cached() is opt-in memoization of a derivation (same relationship @cached has to ordinary getters). Fixed in Summary, Motivation, usage headings, and How We Teach.

NullVoxPopuli-ai-agent added a commit to NullVoxPopuli-ai-agent/ember.js that referenced this pull request Jul 31, 2026
Implements the standalone form proposed in
NullVoxPopuli/rfcs#19 -- the memoization
companion to RFC 1071's overloaded tracked (emberjs#21471).

cached(fn, options?) returns a read-only CachedValue backed by
createCache/getValue. options.equals (default Object.is) retains the
previous value's identity when a re-computation produces an equivalent
result; options.description labels the cache for debugging. The
decorator form is unchanged: a legacy decorator invocation always
receives three arguments, so the two forms cannot collide.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

## Motivation

Our documentation / guides currently don't have much of anything on reactivity, and over the years, it's been useful to talk about reactive primitives as things _outside_ of classes, and compose/wrap them in to refactoring boundaries (classes, components, etc).

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in 1fe2145 — Motivation now links ember-learn/guides-source#2219 instead of claiming the guides are sparse.


Our documentation / guides currently don't have much of anything on reactivity, and over the years, it's been useful to talk about reactive primitives as things _outside_ of classes, and compose/wrap them in to refactoring boundaries (classes, components, etc).

To be clear about terms: _derived state_ is not the same as _cached state_, and derived state outside of a class needs no new API -- any plain function or getter that reads reactive values is already derived state, and staying a plain function should remain the default. `cached` is specifically about _caching_ such a derivation: only re-running the function when tracked state it previously read has changed. As with the `@cached` decorator, this is an opt-in optimization for expensive computations, not the way to derive.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't need to clarify this, because it's not an ambiguation. the docs in ember-learn/guides-source#2219 explain this (I think).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the paragraph in 1fe2145 (also trimmed the echo of it in How We Teach).


Enabling `cached` to be used outside of a class makes it a good tool for demos[^demos] for memoizing expensive computations in function-based APIs, such as _helpers_, _modifiers_, or _resources_ (or even in module space)[^apps]. They also provide a benefit in testing as well, since tests tend to want to assert that expensive computations do not re-run unnecessarily.

This is not too dissimilar to the [Autotracking Memoization primitives in RFC#615](https://github.com/emberjs/rfcs/blob/master/text/0615-autotracking-memoization.md) (`createCache` / `getValue`). Making `cached` work outside of classes provides the same benefit without requiring 2 imports from a `primitives` path to use. This RFC intends to provide a tool enabling us to de-emphasize (and potentially later deprecate) the `@glimmer/tracking/primitives/cache` import path for app developers[^primitives-future].

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't need to deprecate this. we should probably scrap this paragraph and mention it in the Appendix

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scrapped from Motivation in 1fe2145; now an Appendix section ("Relationship to RFC#615's memoization primitives") with no de-emphasize/deprecate language — primitives stay as-is.

}
~~~

Unlike RFC#1071's `TrackedValue`, there is no `set`, `update`, or `freeze` -- a `CachedValue` is derived entirely from the tracked state its function reads, so it is _born_ a `ReadOnlyReactive`.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

born? don't use that word

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reworded in 4f8eb4e — "has no storage of its own; its value comes entirely from the tracked state its function reads, so it is a ReadOnlyReactive from the start."

this.#cache = createCache(() => {
let next = fn();

if (this.#hasPrevious && options.equals(this.#previous, next)) {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

using equals here makes no sense. So this RFC should not propose equals as part of the construction API.

We could add it later, but it's unclear if there would ever be a benefit.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(it makes no sense, because calling fn at all is the expensive part, so if we're going to call it to then not use the value, that's silly)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed equals from the proposal in 4f8eb4e — options are now just { description }, with a short deferred note in the Appendix. Also dropped from the implementation PR (emberjs/ember.js@ea6ee0e).

}
```

(the real implementation would live lower in the reactivity system, because less abstraction layers are speedier)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need to say this

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dropped in 4f8eb4e.


(the real implementation would live lower in the reactivity system, because less abstraction layers are speedier)

The function passed to `cached` is only re-invoked when tracked state it previously read has changed -- exactly the memoization semantics of the `@cached` decorator from [RFC#566](https://github.com/emberjs/rfcs/blob/master/text/0566-memo-decorator.md).

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in this whole RFC, let's not use the word memoization

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 4f8eb4e — "caching" throughout; the only remaining instance is inside RFC 615's URL slug.

Comment thread text/0000-overload-cached-for-non-class-use.md Outdated

Enabling `cached` to be used outside of a class makes it a good tool for demos[^demos] for caching expensive computations in function-based APIs, such as _helpers_, _modifiers_, or _resources_ (or even in module space)[^apps]. They also provide a benefit in testing as well, since tests tend to want to assert that expensive computations do not re-run unnecessarily.

`cached`-as-non-decorator was prototyped in [Starbeam](https://starbeamjs.com/guides/fundamentals/functions.html) (as `CachedFormula`) and similar utilities have been available for folks to try out in ember via [ember-resources](https://github.com/NullVoxPopuli/ember-resources) and [reactiveweb](https://github.com/universal-ember/reactiveweb).

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

re: 'reactiveweb', which one? if not real, remove

re: resources -- not related -- that is a very different concept

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed both in e2a8f89 — prior art now cites Starbeam's CachedFormula only.

);
}

interface CachedValue<Value> extends ReadOnlyReactive<Value> {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's not add a new interface. get: () => Value should be part of read-only reactive, and its an oversight that it's not

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in e2a8f89 — no new interface; get added to ReadOnlyReactive (noted as an RFC 1071 oversight) and cached() returns ReadOnlyReactive<Value>. Implementation updated to match (emberjs/ember.js@49ccf38): get is on the interface, the class is internal-only.


Caching a computation over local state in a template.

```gjs

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this example is bad and contrived. let's not do this

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed in e2a8f89; kept only the module-state example.

Caching a computation over module state.
This is already common in demos.

```gjs

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good example

@NullVoxPopuli-ai-agent
NullVoxPopuli-ai-agent changed the base branch from master to nvp/cached-overloaded July 31, 2026 11:51
NullVoxPopuli-ai-agent and others added 7 commits July 31, 2026 07:51
Derived-state companion to RFC 1071 (overloaded tracked). Re-uses the
Reactive/ReadOnlyReactive interfaces defined there; cached(fn, options)
returns a read-only CachedValue.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Derived state is just plain functions and needs no API; cached() is
opt-in memoization of a derivation. Reword Summary, Motivation, usage
headings, and How We Teach accordingly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Motivation references the in-flight reactivity guides
  (ember-learn/guides-source#2219) instead of claiming docs are sparse
- remove the derived-vs-cached clarification paragraph (the guides
  cover it; not an ambiguation)
- move the RFC 615 relationship to the Appendix, without any
  de-emphasize/deprecate intent

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- remove the equals option from the proposed API entirely (calling fn
  is the expensive part; re-running it to discard the result is silly);
  noted as deferred in the Appendix
- say caching, not memoization, throughout
- reword the 'born a ReadOnlyReactive' sentence
- drop the implementation-would-be-lower aside

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- no new CachedValue interface; add get to ReadOnlyReactive instead
  (an RFC 1071 oversight) and return ReadOnlyReactive from cached()
- apply suggested guides sentence (drop 'over the years')
- Starbeam only in prior art; resources are a different concept
- remove the contrived nested-let template example

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@NullVoxPopuli-ai-agent
NullVoxPopuli-ai-agent force-pushed the overload-cached-for-non-class-use branch from e2a8f89 to 1d1bcb0 Compare July 31, 2026 11:51
@NullVoxPopuli
NullVoxPopuli merged commit e41fbbd into NullVoxPopuli:nvp/cached-overloaded Jul 31, 2026
1 check passed
@NullVoxPopuli
NullVoxPopuli deleted the overload-cached-for-non-class-use branch July 31, 2026 11:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants