rfcs
rfcs copied to clipboard
A new reactive primitive: `cell`
Propose a new reactive primitive: cell
Rendered
Summary
This pull request is proposing a new RFC.
To succeed, it will need to pass into the Exploring Stage, followed by the Accepted Stage.
A Proposed or Exploring RFC may also move to the Closed Stage if it is withdrawn by the author or if it is rejected by the Ember team. This requires an "FCP to Close" period.
An FCP is required before merging this PR to advance to Accepted.
Upon merging this PR, automation will open a draft PR for this RFC to move to the Ready for Released Stage.
Exploring Stage Description
This stage is entered when the Ember team believes the concept described in the RFC should be pursued, but the RFC may still need some more work, discussion, answers to open questions, and/or a champion before it can move to the next stage.
An RFC is moved into Exploring with consensus of the relevant teams. The relevant team expects to spend time helping to refine the proposal. The RFC remains a PR and will have an Exploring label applied.
An Exploring RFC that is successfully completed can move to Accepted with an FCP is required as in the existing process. It may also be moved to Closed with an FCP.
Accepted Stage Description
To move into the "accepted stage" the RFC must have complete prose and have successfully passed through an "FCP to Accept" period in which the community has weighed in and consensus has been achieved on the direction. The relevant teams believe that the proposal is well-specified and ready for implementation. The RFC has a champion within one of the relevant teams.
If there are unanswered questions, we have outlined them and expect that they will be answered before Ready for Release.
When the RFC is accepted, the PR will be merged, and automation will open a new PR to move the RFC to the Ready for Release stage. That PR should be used to track implementation progress and gain consensus to move to the next stage.
Checklist to move to Exploring
- [ ] The team believes the concepts described in the RFC should be pursued.
- [ ] The label
S-Proposedis removed from the PR and the labelS-Exploringis added. - [ ] The Ember team is willing to work on the proposal to get it to Accepted
Checklist to move to Accepted
- [ ] This PR has had the
Final Comment Periodlabel has been added to start the FCP - [ ] The RFC is announced in #news-and-announcements in the Ember Discord.
- [ ] The RFC has complete prose, is well-specified and ready for implementation.
- [ ] All sections of the RFC are filled out.
- [ ] Any unanswered questions are outlined and expected to be answered before Ready for Release.
- [ ] "How we teach this?" is sufficiently filled out.
- [ ] The RFC has a champion within one of the relevant teams.
- [ ] The RFC has consensus after the FCP period.
NOTE: related RFC, tracked-storage-primitive, in implementation here: https://github.com/emberjs/ember.js/pull/20814
I’d like to suggest that we consider alternative names for this feature, such as ref (as seen in frameworks like Vue) or signal (used in Preact, Angular, and others). These names are already familiar to many developers, especially those coming from different frameworks. Adopting such terminology could not only smooth the learning curve but also enhance the "searchability" of the feature in the future, making it easier to discover in documentation and discussions across the broader JavaScript ecosystem.
Aligning with these well-known terms could also foster greater consistency across frameworks, benefiting both new and experienced developers who are already familiar with these patterns.
I also think this feature is a great addition to Ember. As reactive state management becomes a standard in modern frameworks, it's exciting to see Ember adopt this approach, providing developers with more flexibility and a declarative way to build dynamic UIs.
I’d also like to propose introducing a new package for this feature, following a similar approach to the one in RFC #1068. Specifically, we could create a package like @ember/reactive or @ember/reactivity to house this feature and any future reactive state management utilities.
By using a clear, scoped package like @ember/reactive, we can create a dedicated space for related functionality, making it easier to extend and maintain as the Ember ecosystem evolves. This would also allow for more modular imports, helping developers adopt the feature in a clean and consistent manner, such as:
import { ref } from '@ember/reactive';
// or
import { Cell } from '@ember/reactive';
Additionally, this package could pave the way for moving @glimmer/tracking into @ember/reactive in the future, providing a more unified approach to reactive programming within the Ember ecosystem. It would help maintain the modularity and organization of Ember's core features, ensuring that reactive state management tools are accessible in one place.
I like where your head's at!
I'm probably a fan of standardizing on new stuff going in @ember/reactive tbh.
As I've been looking to implement Cell in
- https://github.com/glimmerjs/glimmer-vm/pull/1682
And a similar RFC in:
- https://github.com/emberjs/ember.js/pull/20814
I've found that a lot of stuff has a chain-of-re-exports... which is a bit annoying to work with.
The underlying private packages from glimmer-vm export enough things where we could implement new APIs in ember -- but the tricky part is that if we want new default keywords, they need to go in the VM -- unless the VM provides a way to ember to add keywords (without build-time transforms, hopefully).
So, this is probably an organizational thing we just need to fix. There has been talk of combining the repos -- but that's a giant task that someone probably just needs to do (and hopefully without losing the DX that @wycats has been working on in the VM lately).
But this organizational split is probably why the re-exports exist in the first place -- glimmer-vm needs access to things, but we want better (and public) import paths for our users.
I’d like to suggest that we consider alternative names for this feature, such as ref (as seen in frameworks like Vue)
I don't really agree with ref, because it's a very overloaded term outside of the vue ecosystem.
- short for "reference"
- when searching for "ref javascript", you decidedly get React results, and
refin react is an "element placeholder" (usually), but is also their concept for class properties.ref javascript -reactis the new search, and then you find out about "pass by reference", "referential integrity", etc (good stuff to learn if folks don't know already!)
- "pass by reference"?
- a bonus here: all signal-based libraries are "reactive by reference", in that there is no deep reactivity unless an API explicitly creates deep tracking for each nested reference
or signal (used in Preact, Angular, and others).
Until the signals proposal lands, I'm personally wary of landing on the name "Signal". The proposal in TC39 isn't even sure it wants to stick with that name. Additionally, in all the ecosystems using Signals, they all have wildly different APIs, so I think having a specific name for a specific shape of API would be beneficial as to not confuse with the other ecosystems -- since there is currently not alignment on how these things should work.
While the purpose of the Signals proposal is to unify the frameworks, within each ecosystem, developers are not meant to use signals directly, exactly, but to continue to use each of their ecosystem concepts and APIs. This makes Signals more of a low-level platform feature that app developers could use, and they would just work, but the benefits of using the ecosystem-specific APIs are far greater -- greater ergonomics, greater SEO, etc.
This is why Svelte has "Runes", and why some other reactive frameworks are continuing to use "stores" (or "refs" in vue's case).
For frameworks, Signals are an implementation detail rather than the user-facing API.
Aligning with these well-known terms could also foster greater consistency across frameworks, benefiting both new and experienced developers who are already familiar with these patterns.
This is what the TC39 Signals Proposal is for: once implemented, frameworks can / should change out their underlying primitives for the platform-based signals implementation. No app dev would have to change a single piece of their code, but they'd suddenly be able to use raw signals if they want, and things would just work.
The distinction is important, because already a Cell has more APIs than than a Signal.
As currently proposed at TC39, a Signal instance (for app-devs), has only the following APIs:
get()set()
And that's it.
The Cell here has:
currentread()set()update()freeze()
So with the different set of APIs, and intent to make cell-usage ergonomic in templates (Signals would not be (until we get some kind of expression syntax)), I think it makes more sense to purposefully differentiate the name.
I also think this feature is a great addition to Ember. As reactive state management becomes a standard in modern frameworks, it's exciting to see Ember adopt this approach, providing developers with more flexibility and a declarative way to build dynamic UIs.
:heart: :tada:
Wondering, if it make sense to implement Symbol.toPrimitive property to return this.value, to be able to do simple operations without unwrapping it.
Also, seems we need a way to detect Cell (for internal reactivity usage), like:
// or helper
function or(...args: unknown[]) {
return args.find((arg) => {
return isCell(arg) ? !!arg.value : !!arg;
});
}
In addition, if we talking about primitives, having freeze seems an overhead, because it may be part of public api, exposed by ember.
Also, seems we need a way to detect Cell (for internal reactivity usage), like:
maybe -- but I also don't think folks should be passing cells around without knowing that they're cells.
for rendering tho, I'm thinking that we "collapse" the chains of "reactives":
e.g.:
const a = Cell.create(2);
const b = Cell.create(b);
<template>
{{a}} === {{b}}
renders: 2 === 2
</template>
However, this convenience would probably lead to accidental passing of values without knowing their cells.
All the reason to keep something like this low-level, and not expose to rendering. Like, folks can use them in resources, modifiers, helpers, etc (as these would collapse down to a value), but they probably shouldn't be directly returned from other APIs.
This is primarily an anti-footgun advice though.
I'm not sure how common needing to check for cell-or-not would be, but due to future plans with similar APIs with current/read, I'd probably want something like:
isReactivePrimitive(cell /* or formula (classless cached) */)
resources could go in here, but there is a convention right now around calling a function first that return a resource -- this is typically where longer-lived state resides, and I don't think it makes sense to support using them in helpers (since helpers don't typically have lifetime) -- it'd make more sense to only allow resources to be composed minimally within resources, or any other higher-level concept -- this means that resources would not be a reactive primitive, but more some sort of lifetime primitive that has reactivity. This also lead mo to talk myself out of being opposed to isCell, because all that's left is a classless cached (formula) and that's not really cell, but would have similar APIs (tho, read-only).
HMMM
@NullVoxPopuli, vue exposing it's reactive primitives (with composition api), signals soon will be a thing, and I think we should count that because users will see reactive cells in real life anyway, and if we hide our, we may get more wrappers around it with compute and memory overhead.
A note on the choice of current vs (something like value):
current communicates more meaning than value as current implies that the value is specifically up to date and cannot be out of date.
value alone has no implications of history, up-to-date-ness, etc.
I was wondering about how exactly this relates to future TC39 Signals, and your long comment was very helpful in that regard. Maybe for future travellers a gist of that should be in the RFC, and maybe stating that once Signals eventually land, this would prevode the main integration point for them (subject to a future Signals integration RFC)?
we could create a package like @ember/reactive or @ember/reactivity to house this feature and any future reactive state management utilities.
Strong +1 from me!
A note on the choice of
currentvs (something likevalue):
currentcommunicates more meaning thanvalueascurrentimplies that the value is specifically up to date and cannot be out of date.valuealone has no implications of history, up-to-date-ness, etc.
I think it's only one time learn thing, and there is no real reason to have current name. Because most state buckets use value, and it's easy to learn and adopt (from prev experience).
If we have current that we "equal" to value?
If we have current, why we don't have previous and upcoming?
Current reminds me of https://en.wikipedia.org/wiki/Electric_current meaning, and not about current value because it's always current (actual).
Also, current has stream meaning, and having Rx vibes.
Imho, we should not do CS here, we should keep things simple in terms of naming and not create extra wording.
Of note, a "TrackedArray" created with https://github.com/glimmerjs/glimmer-vm/pull/1682 and no length-cell is 5x faster than the TrackedArray from tracked-built-ins.
A note on the choice of
currentvs (something likevalue):currentcommunicates more meaning thanvalueascurrentimplies that the value is specifically up to date and cannot be out of date.valuealone has no implications of history, up-to-date-ness, etc.I think it's only one time learn thing, and there is no real reason to have
currentname. Because most state buckets usevalue, and it's easy to learn and adopt (from prev experience).If we have
currentthat we "equal" tovalue? If we havecurrent, why we don't havepreviousandupcoming?Currentreminds me of en.wikipedia.org/wiki/Electric_current meaning, and not aboutcurrent value because it's always current (actual). Also,currenthasstreammeaning, and havingRxvibes.Imho, we should not do CS here, we should keep things simple in terms of naming and not create extra wording.
I second that. From an API perspective alone, if there is a .current prop, does that also mean there is a .previous or .next (similar to iterators)? Or if this is only the "latest" value, is there a history: Value[] to it?
As much as const, let or var do not carry on their history of past values, neither do I see the need for Cell as well. Cell is the reactive layer on top, but both have the same mnemonics. To me value is more clear and less ambigous.
Let's make it clear that this RFC also effectively amends the earlier tracked storage primitives RFC to say that that one (which has no official implementation anyway) got superseded by this one.
At RFC review we discussed whether we really need Cell.prototype.set, since assignment of current means the same thing. It's true that closing over assignment in our templates is not currently possible, but we should fix that for all cases, not just Cell. If we keep set we should document very clearly that it's only a convenience, and that assignment to current is the fundamental operation.
I also reiterated that I'm really not comfortable trying to use Cell as a foundational primitive without doing the "other half" of the design. The part that explains the low-level API for actually observing and reacting to Cell consumption.
Resources RFC has started here: https://github.com/emberjs/rfcs/pull/1122
Ergonomics of Resources will somewhat depend on this RFC
Coming here from the resources RFC.
I understand the prior art with starbeam and the work that lead into signal proposal.
This is the one thing, that keeps me irritation, the word cell() (am I the only one thinking about DBZ/biology now?). I understand this as part of research but I wouldn't consider the name as final and subject to change as part of this RFC.
The name itself seems quite odd for what it does. For reference: vue uses ref(), svelte uses $state() and solid is using signal.
Since it is embers signal impl, I'd be more happy to take a name more related towards that (same with .value over .current).
What about Signal compatibility?
I have been making a vanilla js library, and made an adapter for frameworks to provide their signal impl. Here is the one I made for ember:
import { cell } from 'ember-resources';
const signalFactory: SignalFactory = <T>(t?: T) => {
const reactive = cell(t);
return {
get(): T {
return reactive.current;
},
set(val: T) {
reactive.set(val);
}
};
};
and it works fantastically well.
I used the TC39 signal RFC for my reference, and signalFactory essentially returns a Signal.State object.
Now "cell" is basically Embers spiritual implementation of a Signal within embers tracking context.
Shouldn't it implement the Signal spec then, allowing to do the above in just that:
import { signal } from 'ember/reactivity';
const signalFactory: SignalFactory = <T>(t?: T) => signal(t);
I found it weird and wrong to use a wrapper for this. The argument for will be the technical implementation differs in nuances. Practically we will all carry around the same wrapper.
Ideally agnostic libraries support signals and you can stuff in any reactive value from any framework and I would like Ember to support this. Similarly to how Standard Schema works. Maybe, there is a ~signal prop that would be cool, too.
I know signal RFC is still under development and this can be defered, but a note on making this compatible would suffice (if there is, I overread it).
What about Signal compatibility? Similarly to how Standard Schema works. I know signal RFC is still under development and this can be defered, but a note on making this compatible would suffice (if there is, I overread it). Shouldn't it implement the Signal spec then
Signals are an implementation detail - when/if TC39 lands Signals, all our internals will be swapped for TC39's Signals (e.g.: we'll no longer need tags) That doesn't change the Cell's interface though, because we have different / additional ergonomics desires:
- current
- set
- update
- freeze
- read
Now, we can add a get() super easy if that ends up being a deal breaker. I initially shyed away from it because Ember has a history of bad associated with get/set, but interface matching is nice.
This is something that can be added on to later. It's much easier to add APIs than re-work something that is fundamentally wrong in already in use.
And! this is what the proposal encourages. The TC39 Signal's proposal encourages wrappers for frameworks to provide their own ergonomics around the lowest level primitive. We don't want users to be using Signals directly, because we believe we can provide a better out of the box experience (while at the same time, we know we have to be 100% compatible with folks who do use the lowest level APIs).
[!NOTE] TC39's Signals are still Stage 0 -- and while we can make sure that we don't make ourselves incompatible what TC39 is doing, it's far too early to assume their APIs aren't going to change -- they haven't even gathered implementor's feedback yet -- thinks could be vastly different by the time the proposal gets to Stage 1, let alone Stage 3.
I found it weird and wrong to use a wrapper for this.
Firstly, it's a bit too early to be using TC39 Signals. Secondly, if your goal is cross-ecosystem reactivity compatibility, there is no escaping this -- you need to create a signal for each reactive system you're integrating with, which will require a wrapper anyway (I want to RFC a new library for ember to wire this up, based on what Starbeam and WarpDrive have been playing with)
I'm experimenting right now with integrating other libs into ember. Some of them already have a signal-ish implementation which makes it compatible amongst other frameworks.
Signals are an implementation detail
I highly disagree with that statement here. It is about compatibility (or the intend) to integrate with third party libraries, that work with signals[^1].
Here is a nice example I find in integrating better-auth with Ember. By nature I used a servive in an addon. Only to figure I neither need a service nor an addon (stand today), using their client is enough, it has support for "signals" built-in. Here is how to use:
import { createAuthClient } from 'better-auth/client';
export const auth = createAuthClient();
then there is a auth.useSession prop, that is reactive and integrated in a couple of frameworks with their provided wrappers. Here is sample impls:
- Vue: https://github.com/better-auth/better-auth/blob/eca7fc5ec7f41013c167fa8c79909d27316e5bcc/packages/better-auth/src/client/vue/index.ts#L58
- Svelte: https://github.com/better-auth/better-auth/blob/eca7fc5ec7f41013c167fa8c79909d27316e5bcc/packages/better-auth/src/client/svelte/index.ts#L51
That is, this is built on top of nanostores and auth.useSession is an atom.
Goal should be to have compatibility with these (I picked better-auth as one example).
Signals are an implementation detail - when/if TC39 lands Signals, all our internals will be swapped for TC39's Signals (e.g.: we'll no longer need tags)
That's cool.
That doesn't change the Cell's interface though, because we have different / additional ergonomics desires:
current set update freeze read
Why do need this stuff then? Aren't Signals enough? Is this a requirement from the VM itself? I may not have run into the situation to need them.
But reading on:
Is update() similar to untrack() in solid? Can it be used to combat the infamous: You attempted to update 'prop' on 'Object' but it had already been used previously in the same computation - if so, this is more than welcome 😬
read() and current still fall out of place. They do the same job as value or get() in all the other implementations and are semantically equal. There is no reason to make a deviation other than make it harder to learn and increase the error rate on interop with others.
PS. Cell is still the most horrible name here. We are no managers, we do not play spreadsheets - give use a name for engineers.
Or I throw in calling it Nerve, because computeds can then be named Reflexes (that's a lovely analogy).
[^1]: By signals, I mean the current state of impl, with slight wrappers for each framework. With the aim to remove that once TC39 signals RFCs lands.
Why do need this stuff then? Is this a requirement from the VM itself?
There are bigger plans around this, which @ef4 is rightfully pressuring to me write out -- around the next phase of our renderr.
In general though, the things you point out are for optimizations and ergonomics -- aiding in swapping out the VM with a new renderer.
- freeze would tell the renderer and reactivity system to not bother with this value ever again (handy for DOM stuff, for example)
- current is already explained in the RFC -- it has a semantic meaning, and is more versatile than a method invocation
- set - this matches a signal api, which is what you're arguing for
- read - this matches a signal api, tho alias of get, which is what you're arguing for
- update - as you point out, yea, this gives folks a guaranteed safety to read-then-write
Aren't Signals enough?
nay. Signals don't provide any of the above (aside from set/get/read)
naming
Everyone naming their non-signals signals is a causing a problem. Because then to specifically talk about the difference, you need more words.
name for engineers
naming for engineers is arguably something I think we should avoid, as we should strive to be understood by humans, rather than scratch the beards of fellow intellects <3
overall
it's clear to me that the gaps left here in how my RFCs fit in to the bigger picture are .. noticable -- so my next step is to prototype out a little thing for our next phase of rendering and reactivity, and then RFC based on that -- this will basically be the Starbeam RFC
naming for engineers is arguably something I think we should avoid, as we should strive to be understood by humans, rather than scratch the beards of fellow intellects <3
But engineers are humans, no? This is a technical framework so its okay to have technical naming practices... Developer eXperience should be the focus here, not the end-users of products created using ember.