From 99159ee5120158130572d083cfc8ef405cabb6fb Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Thu, 6 Mar 2025 16:07:46 -0500 Subject: [PATCH 01/16] Begin reactive() --- text/0000-ember-reactive.md | 321 ++++++++++++++++++++++++++++++++++++ 1 file changed, 321 insertions(+) create mode 100644 text/0000-ember-reactive.md diff --git a/text/0000-ember-reactive.md b/text/0000-ember-reactive.md new file mode 100644 index 0000000000..dbefc76e0b --- /dev/null +++ b/text/0000-ember-reactive.md @@ -0,0 +1,321 @@ +--- +stage: accepted +start-date: 2025-03-06T00:00:00.000Z # In format YYYY-MM-DDT00:00:00.000Z +release-date: # In format YYYY-MM-DDT00:00:00.000Z +release-versions: +teams: # delete teams that aren't relevant + - framework + - learning +prs: + accepted: # Fill this in with the URL for the Proposal RFC PR +project-link: +suite: +--- + + + + + +# Propose a simpler tracked state utility from `@ember/reactive` + +## Summary + +The RFC proposes solving two things: +- import fatigue +- reactive-wrapper fatigue + +by introducing a brand new package (one additional import: `@ember/reactive`) with a named export, `reactive` which allows deep reactivity across all our default reactive data structures in the current blueprint: +- primitive values (only possible via decorator) +- arrays +- objects +- maps +- sets +- weak maps +- weak sets + + +## Motivation + +Example[^example-requires-vm-work]: +```gjs +import { reactive } from '@ember/reactive'; + +const startingData = { greeting: 'hi' }; +const exclaim = (data) => data.greeting += '!'; + +export const Demo = ; +``` + +
class-based example + +Same footnote applies as above[^example-requires-vm-work] + +```gjs +import Component from '@glimmer/component'; +import { reactive } from '@ember/reactive'; + +export class Demo extends Component { + // makes the *reference* reactive + @reactive count = 0; + + // makes the right-hand side of the = reactive + data = reactive({ greeting: 'hello' }); + + // both left and right-hand sides are reactive + @reactive data2 = { greeting: 'hello' }; + + exclaim() { + this.count++; + this.data.greeting += "!"; + this.data2.greeting += "!"; + } + + +} +``` + +
+ +[^example-requires-vm-work]: These examples assume that these RFCs are implemented: [#989: `fn` as a keyword](https://github.com/emberjs/rfcs/pull/998), [#997: `on` as a keyword](https://github.com/emberjs/rfcs/pull/997), [#1070: Default globals for strict mode](https://github.com/emberjs/rfcs/pull/1070), and that the long-standing bug in the VM where plain methods don't work in class-based components is fixed. + +Feedback on prior RFCs, [The Cell](https://github.com/emberjs/rfcs/pull/1071), and [tracked-built-ins, built-in](https://github.com/emberjs/rfcs/pull/1068), revealed that folks have found that building reactive data structures, by default, is cumbersome. While there utilities would not be removed in anyway, we want to provide a way for folks to make things easily reactive, and _still_ provide a way to get fine-grained reactivity via the existing tools that folks are used to today. + +The main tradeoff with deep reactivity vs explicit / shallow is the impact to memory. This RFC describes those tradeoffs, and provides a way for folks to determine when they may want to consider + +This doesn't eliminate or remove any existing APIs, but, being a low-level implementation would + +## Detailed design + +Goals for `reactive`: +- decorator and non-decorator usage (compatible with typescript) +- handles both primitive values and the _common collections_[^the-common-collections] + +[^the-common-collections]: The comment collection data structures are: `Array`, `Set`, `WeakSet`, `Object`, `Map`, `WeakMap` + +### Proposed API / available usage + +[Example Types at tsplay.dev](https://www.typescriptlang.org/play/?#code/JYWwDg9gTgLgBAbzgUwB5mQYxgFQJ4YDyAZnAL5zFQQhwDkaG2AtDAcnQNwBQ3A9HzgALGDDABnAFwCA5sBhCArgCMAdJhp9kIZcigArcXyjFMRsIoA2lvgEYADAHZbfYsEvJx3YADsYe4gBDTGQ4ACVkYJhgADdkAB4ANUDLRWQAPkRuODhMRSgoZD9JOGTU5B4cwsCAEwAKAEoSsrSeMl5ffxNg0IBhZGsklLTMtH8fGvFwyOxYhJaMrJyBOFU17nb+QTBLQN84RXFAmWRuYkUfWYgfOGrZuKHy9LrfCxhm4eQm0s+eFfEYMdQi4AExwGpYaCBGDQM4XK43O7ROJ1QFQE7vOAQZT6LAwAA0cAA1sg8CUAVBfDI4AAfODiPA6CCWQkQ8SYSlgGFQAD8JQuRJ8EAA7j5vgAFagYWB4AAikKg0Ogf0E4iYcEEOF6AGYAJxweUaRXcuGXaLXW4zZHzT7PNEYkq9XbicQAQUwIRd0ENUO5OEC6OQMHiAqFosJC3ShI0fjGjudbo9nnE3oVSqgvWu-lQMG+TsCLvdnpTUB9xugEXEVmDoZFPgjtpVcFAO20RUB5puqPY4Ignh8dHgIGhXUJaCiljwDVNCMtUTmjxGLx8bw+5W+CyWlpg+Ru9jgBYOPkFdYPUwWbV4WwEN9vt62cBwnhg4gAhNe75++N5Y90QnAAGUaGQQgcS3YgIAgEofEUHQ9EqOBlADaDkDiKBLwfbFcWwbhGDxfAiGIOokTmOokAgqC4DBMgGgaVQYQAWWhTAhFA7DcHYeJyMg6DYN0KByGeBomwDRU8Fw9B8PYEhiKtUiAG06AougAF1aPoiAAFEAEdFBSAiQOIeIKSpeSVKEpthzACSmA4wjZPnFEfGQYU4CYsBjJgSkfBkQkTJ8oT1JhHS9MsAySHidzPO83z6S8ql0gsh9hUiIk4CsmypPskinJcuAAHVUqioCQBAnFCWUSCPECHxAro4LdP06SjMKwIiWK4C2IqqrIlqpL-iDTLsHCoicuQOpnNcgCg2ihLGiCrTGrC5r4mm4N-JkRLGibFK2vpQa8OG5qHPucbJoK1K1tWzqcTqjSQqawj4laokrpKsr9C24TeBWCEjXTLxGGgeBMATOAADFIK3FZKoUOdTstYg9CKf8apqeHrTgGJPmyOAAAExtyCALngABeOB7AQgm5LiA84HJ7jKOoqnCeUem4EU5SVJZmnQkwdnzqija-PigLttx6nHNCdHyfOl6OtKrrEJ6mqkpySWEdCWW8quja1fxwnSG11yXrem7PvF3GVkJ7HyixHxJ0JQokcKS5QmAKYAWhYBMFx6lybGup7G+nIhHZwPGZKEEaIQ4Bw95upOcg1SQ7gfR46lia8qF0XYr1+bcbSgOE7lorAg896lcq5lesChDLAz06s6mmb89ohDaGLzPS7as3Fdu+aeCtwRGEsH35AxuZEeRt2DwmSfadttIJcJm4u6b4OealrFG+tMjKB4qiY5X3m4DAXeFKU5O1K3hHtIv3LXJzmKRZiwKT+3gT173nv2vL67+76G6jXVWg8P4IymN-Ui51da5zruAzGZMF5nTyqbGalcB7tw2LwDw8AjZwHOpDCAltDp2UMnUYg6hiZ+HqotUKI14gwTglAJKpCRoUNUIEWhD1lpPQQBRXizDBLizYcdShyhuFLQYRtMyrDJJHXspQzAkj6ErWflSV+CU5G2XYZQmoKjHqGWemXCu5tgHVT6iI+RZCZKUK+PdKRK1YFv20VlchlDiAGN4UYtBwYMGfSSkNGxRFKEyC8Qwph-FXEKPcaoIQ4SVr8MPpEvQwjvqiMUaoYACSnoyPMlYnRYjVD6ByUY9RPlNEBWicEjhRJSkRXlv-fx5ja7VN0aoSw9SjLOK0QUtxtjVAgC6cY3u6DzZfSHhk2JYoHGqKeiklhfSYkDIgMMpJlEFlpJ4FMgZYBhl5LaUU7Swzyl5zgYczJUBhmNNMYAlpqsLmxPEMMnpVSlk1MobmWZhiGmXTGYAiZvAgA) + +
example type tests + +```ts +import { expectTypeOf } from 'expect-type'; + +// https://github.com/emberjs/rfcs/pull/1071/files +interface Reactive { + current: Value; + read(): Value; +} + +interface Cell extends Reactive { + // ... +} + +// plain usage +function reactive(input: Value): Value; +// TC39 Stage 1 or 2 Decorator +function reactive(target: object, key: string | symbol, descriptor?: unknown): PropertyDecorator; +// TC39 Stage 3 or 4 Decorator (what is shipping to browsers) +function reactive(target: ClassAccessorDecoratorTarget, context: ClassAccessorDecoratorContext): ClassAccessorDecoratorResult; +// implementation (type doesn't matter, exactly) +function reactive(input: Value): Value { + return 0 as unknown as Value; +} + + +/////////////// +// Tests! +/////////////// +interface SomeObj { + foo: number; + bar: never; +} + +// object +expectTypeOf(reactive({ foo: 2 })).toMatchObjectType<{ foo: number }>(); +// array +expectTypeOf(reactive(['foo'])).toEqualTypeOf(); +// map +expectTypeOf(reactive(new Map())).toEqualTypeOf>(); +// weak map +expectTypeOf(reactive(new WeakMap())).toEqualTypeOf>(); +// set +expectTypeOf(reactive(new Set())).toEqualTypeOf>(); +// weak set +expectTypeOf(reactive(new WeakSet())).toEqualTypeOf>(); + +// decorators +export class Foo { + // both reactive reference and reactive value + @reactive count = 0; + @reactive a = { foo: 2 }; + @reactive b = ['foo']; + @reactive c = new Map(); + @reactive d = new WeakMap(); + @reactive e = new Set(); + @reactive f = new WeakSet(); + + // reactive value only, reference is static + g = reactive(0); + h = reactive({ foo: 2}); + i = reactive(['foo']); + j = reactive(new Map()) + k = reactive(new WeakMap()); + l = reactive(new Set()); + m = reactive(new WeakSet()); + + // explicit reactive reference and reactive value + @reactive n = reactive(0); + @reactive o = reactive({ foo: 2}); + @reactive p = reactive(['foo']); + @reactive q = reactive(new Map()) + @reactive r = reactive(new WeakMap()); + @reactive s = reactive(new Set()); + @reactive t = reactive(new WeakSet()); +} + +let f = new Foo(); + +expectTypeOf(f.count).toEqualTypeOf(); +expectTypeOf(f.a).toEqualTypeOf<{foo: number }>(); +expectTypeOf(f.b).toEqualTypeOf(); +expectTypeOf(f.c).toEqualTypeOf>(); +expectTypeOf(f.d).toEqualTypeOf>(); +expectTypeOf(f.e).toEqualTypeOf>(); +expectTypeOf(f.f).toEqualTypeOf>(); + +expectTypeOf(f.g).toEqualTypeOf(); +expectTypeOf(f.h).toEqualTypeOf<{foo: number }>(); +expectTypeOf(f.i).toEqualTypeOf(); +expectTypeOf(f.j).toEqualTypeOf>(); +expectTypeOf(f.k).toEqualTypeOf>(); +expectTypeOf(f.l).toEqualTypeOf>(); +expectTypeOf(f.m).toEqualTypeOf>(); + +expectTypeOf(f.n).toEqualTypeOf(); +expectTypeOf(f.o).toEqualTypeOf<{foo: number }>(); +expectTypeOf(f.p).toEqualTypeOf(); +expectTypeOf(f.q).toEqualTypeOf>(); +expectTypeOf(f.r).toEqualTypeOf>(); +expectTypeOf(f.s).toEqualTypeOf>(); +expectTypeOf(f.t).toEqualTypeOf>(); +``` + +
+ +#### With primitive values + +```gjs + +``` + +#### Decorator + primitive value + +#### With the _common collections_[^the-common-collections] + + + +### Behavior + +#### What does reading do? + +#### What does setting do? + +### What are the performance concerns? + + + +## How we teach this + + + +## Drawbacks + + + + +## Alternatives + + + +## Unresolved questions + + From 8f30bc20d8dd452f4213246be4404bd0277b06ed Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Thu, 6 Mar 2025 16:11:12 -0500 Subject: [PATCH 02/16] Rename --- text/{0000-ember-reactive.md => 1079-ember-reactive.md} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename text/{0000-ember-reactive.md => 1079-ember-reactive.md} (99%) diff --git a/text/0000-ember-reactive.md b/text/1079-ember-reactive.md similarity index 99% rename from text/0000-ember-reactive.md rename to text/1079-ember-reactive.md index dbefc76e0b..e2778e2d72 100644 --- a/text/0000-ember-reactive.md +++ b/text/1079-ember-reactive.md @@ -7,7 +7,7 @@ teams: # delete teams that aren't relevant - framework - learning prs: - accepted: # Fill this in with the URL for the Proposal RFC PR + accepted: https://github.com/emberjs/rfcs/pull/1079 # Fill this in with the URL for the Proposal RFC PR project-link: suite: --- From 9e840f1d6372a05d4d9b3ab497a248d9160b296d Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Thu, 6 Mar 2025 17:23:35 -0500 Subject: [PATCH 03/16] Some examples --- text/1079-ember-reactive.md | 131 ++++++++++++++++++++++++++++++++---- 1 file changed, 119 insertions(+), 12 deletions(-) diff --git a/text/1079-ember-reactive.md b/text/1079-ember-reactive.md index e2778e2d72..72e3984045 100644 --- a/text/1079-ember-reactive.md +++ b/text/1079-ember-reactive.md @@ -117,7 +117,7 @@ Goals for `reactive`: ### Proposed API / available usage -[Example Types at tsplay.dev](https://www.typescriptlang.org/play/?#code/JYWwDg9gTgLgBAbzgUwB5mQYxgFQJ4YDyAZnAL5zFQQhwDkaG2AtDAcnQNwBQ3A9HzgALGDDABnAFwCA5sBhCArgCMAdJhp9kIZcigArcXyjFMRsIoA2lvgEYADAHZbfYsEvJx3YADsYe4gBDTGQ4ACVkYJhgADdkAB4ANUDLRWQAPkRuODhMRSgoZD9JOGTU5B4cwsCAEwAKAEoSsrSeMl5ffxNg0IBhZGsklLTMtH8fGvFwyOxYhJaMrJyBOFU17nb+QTBLQN84RXFAmWRuYkUfWYgfOGrZuKHy9LrfCxhm4eQm0s+eFfEYMdQi4AExwGpYaCBGDQM4XK43O7ROJ1QFQE7vOAQZT6LAwAA0cAA1sg8CUAVBfDI4AAfODiPA6CCWQkQ8SYSlgGFQAD8JQuRJ8EAA7j5vgAFagYWB4AAikKg0Ogf0E4iYcEEOF6AGYAJxweUaRXcuGXaLXW4zZHzT7PNEYkq9XbicQAQUwIRd0ENUO5OEC6OQMHiAqFosJC3ShI0fjGjudbo9nnE3oVSqgvWu-lQMG+TsCLvdnpTUB9xugEXEVmDoZFPgjtpVcFAO20RUB5puqPY4Ignh8dHgIGhXUJaCiljwDVNCMtUTmjxGLx8bw+5W+CyWlpg+Ru9jgBYOPkFdYPUwWbV4WwEN9vt62cBwnhg4gAhNe75++N5Y90QnAAGUaGQQgcS3YgIAgEofEUHQ9EqOBlADaDkDiKBLwfbFcWwbhGDxfAiGIOokTmOokAgqC4DBMgGgaVQYQAWWhTAhFA7DcHYeJyMg6DYN0KByGeBomwDRU8Fw9B8PYEhiKtUiAG06AougAF1aPoiAAFEAEdFBSAiQOIeIKSpeSVKEpthzACSmA4wjZPnFEfGQYU4CYsBjJgSkfBkQkTJ8oT1JhHS9MsAySHidzPO83z6S8ql0gsh9hUiIk4CsmypPskinJcuAAHVUqioCQBAnFCWUSCPECHxAro4LdP06SjMKwIiWK4C2IqqrIlqpL-iDTLsHCoicuQOpnNcgCg2ihLGiCrTGrC5r4mm4N-JkRLGibFK2vpQa8OG5qHPucbJoK1K1tWzqcTqjSQqawj4laokrpKsr9C24TeBWCEjXTLxGGgeBMATOAADFIK3FZKoUOdTstYg9CKf8apqeHrTgGJPmyOAAAExtyCALngABeOB7AQgm5LiA84HJ7jKOoqnCeUem4EU5SVJZmnQkwdnzqija-PigLttx6nHNCdHyfOl6OtKrrEJ6mqkpySWEdCWW8quja1fxwnSG11yXrem7PvF3GVkJ7HyixHxJ0JQokcKS5QmAKYAWhYBMFx6lybGup7G+nIhHZwPGZKEEaIQ4Bw95upOcg1SQ7gfR46lia8qF0XYr1+bcbSgOE7lorAg896lcq5lesChDLAz06s6mmb89ohDaGLzPS7as3Fdu+aeCtwRGEsH35AxuZEeRt2DwmSfadttIJcJm4u6b4OealrFG+tMjKB4qiY5X3m4DAXeFKU5O1K3hHtIv3LXJzmKRZiwKT+3gT173nv2vL67+76G6jXVWg8P4IymN-Ui51da5zruAzGZMF5nTyqbGalcB7tw2LwDw8AjZwHOpDCAltDp2UMnUYg6hiZ+HqotUKI14gwTglAJKpCRoUNUIEWhD1lpPQQBRXizDBLizYcdShyhuFLQYRtMyrDJJHXspQzAkj6ErWflSV+CU5G2XYZQmoKjHqGWemXCu5tgHVT6iI+RZCZKUK+PdKRK1YFv20VlchlDiAGN4UYtBwYMGfSSkNGxRFKEyC8Qwph-FXEKPcaoIQ4SVr8MPpEvQwjvqiMUaoYACSnoyPMlYnRYjVD6ByUY9RPlNEBWicEjhRJSkRXlv-fx5ja7VN0aoSw9SjLOK0QUtxtjVAgC6cY3u6DzZfSHhk2JYoHGqKeiklhfSYkDIgMMpJlEFlpJ4FMgZYBhl5LaUU7Swzyl5zgYczJUBhmNNMYAlpqsLmxPEMMnpVSlk1MobmWZhiGmXTGYAiZvAgA) +[Example Types at tsplay.dev](https://www.typescriptlang.org/play/?experimentalDecorators=true&target=99&jsx=0#code/JYWwDg9gTgLgBAbzgUwB5mQYxgFQJ4YDyAZnAL5zFQQhwDkaG2AtDAcnQNwBQ3A9HzgALGDDABnAFwCA5sBhCArgCMAdJhp9kIZcigArcXyjFMRsIoA2lvgEYADAHZbfYsEvJx3YADsYe4gBDTGQ4ACVkYJhgADdkAB4ANUDLRWQAPkRuODhMRSgoZD9JOGTU5B4cwsCAEwAKAEoSsrSeMl5ffxNg0IBhZGsklLTMtH8fGvFwyOxYhJaMrJyBOFU17nbuNgw4AAUoUHk5uABeOB9FHT04AB84cRgDnxlb86tLV8UJ5DcfZBrXsoIBAPIEfDx+IIwJZAr44IpxIEZMhuMQvrMID44NVZnEhuV0nVfBYYM1hsgmqVyShUONJnsDiAjnE4AB+OD9QYLTJk8o8FYPJGhFwAJjgNSw0ECMGgqPR0Ux2Jm0TidRggSgyNJcAgyn0WBgABo4ABrZB4EoPJ4vO7iPA6EHGiXiTAHMAyqCskpfE0+CAAdx8lLBeH5gnETDgghwvQAzABOOAAEUlUGlsrRPgxWJxKvm5MJ2Tg6s1yG1vRh4nEAEFMCEq9AUxo0x6cBqtfEfX7A8bucaixo-GMShXAlXa-XxI3U+moL1Mf5adxKaPx3XPFOoE2pR6IuIrDBOz5fQGfL2CxCVqBodoiuqFVi1exxRBPD46PAQNKusa0FFLHgDRylmD5KlEcz4iMdRrKo7ZSPCx7dj4ADaAC6lJdqeSxKjA+RYvYcBjghJ6BoRUwLG0vCQgING0bRkJwDgngwOIACE1F0ZxfDeEO3QhHAADKNDIIQerYcQwIlBcVxQJUcDKBqUnIHEskbLwKy6vq2DcIwBr4EQxB1Lmcx1EgEkQCUYpkA0DSqDKACy0qYEIolabg7DxGZklvDJ5CEg0YaEQUgR4Dp6B6ewJBGcqJnIXQ5l0OhtkygAogAjooKT6SJxDxFavgyGh-mBV+YBhUw7kGdF4Gqn8-pwI5YB5Y8BXGvlzz+TZdkQOlmWWNlJDxI1zXWm1LUdcVDH+pEJpwKV5URVVxm1cg9UAOozcNQkgCJerGkCIKRD4nXJT1GVZZFuUbYEJpbcJrn7cCoLHZNAplgt2ADYZy3IHUdWCWWI0FSd3W9RdBnxAJgPtTI6SvYI003fc726Z9l3Vbiv3-ddJpQ4e227foIOped-WXfEON45D916nDjSXoIErNrOXiMNA8CYJWUwAGLAthKxAgoYGY0qxB6EU-FggCP1wDE5JFgAAjLGhfPAZz2HJSsxSygSnIglDeVZmsy8oetxQlqHG9roSYHr-3DTDY3WpNORazVoQAmc2ObYETUEw98lPUdLtwG7IuhF7q0A4eMMh2HeaUHbUeU4D-u0-TvDLIIMty+UOo+ABxqFGLhRZqEwBTIK0SYEWLxnD9dQEUR0m6FAAVFkIesN15FlwCK1lycAXfW3U5vAol7c5Pow-u39UcO+NMhO8DjRATks31yP3s3XdO0Bwdz2dXJHyb7P-1U7Hq9ybQp+Y3P60zVTadE1fmdRoIjCWMAmDyMLCfF+LMuhEJh-2OLnNIisZZYlvnmRuZEfKt0nqHGWEAZ53x7pZAekDrZwDAGg2BY8IATytu7OAaV8EmXtr7IGzxl4dVXtg0hUAKErQfjvahz9HqHTBEfRhIspgwMoVHC+i9eGuxlmrUBrC4Ap3xjTF+NlKLcA8PAUgkd6q8wgBnD6lUcp1GIOoCAqtTpgzJhDFuehJqo10VFAxgQTGky+p5cyUlLitz8hnaxX19GqGUA4vqTiYZFU8eFNGVUDGYH8eDHKQ1qGO3uKIqxoSbGGQMTUKJZiYk413oTLhh8kkVW8QYikoNHHkxEc7Api09EGOIBkpxsjqZ71ppNHRRTVAyHqeTTklh4gWKgHTduXj0YGKEF0iGCAXEIOuGQKpYSamqGAOMmJQTUJzJST4-QyzBoL1GgkypITCkjNUCabZV0fZ+3kXk4O6z2mWDOZDaGiTDnVNsaoEADzGnP0GRCYZ4TVBBlKQE7pAxen9J+W045EAHmTO8v0jxQzkntLAA81ZtzjlpQebs1q+zgbov+W3IF0TBrZI4VcwO3CXovPmW88QDyKl4upRsgxMBPmP1TvIiF3AgA)
example type tests @@ -134,14 +134,20 @@ interface Cell extends Reactive { // ... } +type Primitive = number | string | null | undefined | boolean; + // plain usage -function reactive(input: Value): Value; -// TC39 Stage 1 or 2 Decorator -function reactive(target: object, key: string | symbol, descriptor?: unknown): PropertyDecorator; -// TC39 Stage 3 or 4 Decorator (what is shipping to browsers) -function reactive(target: ClassAccessorDecoratorTarget, context: ClassAccessorDecoratorContext): ClassAccessorDecoratorResult; +function reactive(input: Value): Value extends Primitive ? Cell : Value; +// stage 1/2 decorator +function reactive(target: object, key: string | symbol, descriptor?: unknown): any; +// spec / TC39 Decorator +function reactive( + target: ClassAccessorDecoratorTarget, + context: ClassAccessorDecoratorContext +): ClassAccessorDecoratorResult; + // implementation (type doesn't matter, exactly) -function reactive(input: Value): Value { +function reactive(...args: unknown[]): unknown { return 0 as unknown as Value; } @@ -179,7 +185,7 @@ export class Foo { @reactive f = new WeakSet(); // reactive value only, reference is static - g = reactive(0); + g = reactive(0 as number); h = reactive({ foo: 2}); i = reactive(['foo']); j = reactive(new Map()) @@ -188,7 +194,7 @@ export class Foo { m = reactive(new WeakSet()); // explicit reactive reference and reactive value - @reactive n = reactive(0); + @reactive n = reactive(0 as number); @reactive o = reactive({ foo: 2}); @reactive p = reactive(['foo']); @reactive q = reactive(new Map()) @@ -207,7 +213,7 @@ expectTypeOf(f.d).toEqualTypeOf>(); expectTypeOf(f.e).toEqualTypeOf>(); expectTypeOf(f.f).toEqualTypeOf>(); -expectTypeOf(f.g).toEqualTypeOf(); +expectTypeOf(f.g).toEqualTypeOf>(); expectTypeOf(f.h).toEqualTypeOf<{foo: number }>(); expectTypeOf(f.i).toEqualTypeOf(); expectTypeOf(f.j).toEqualTypeOf>(); @@ -215,7 +221,7 @@ expectTypeOf(f.k).toEqualTypeOf>(); expectTypeOf(f.l).toEqualTypeOf>(); expectTypeOf(f.m).toEqualTypeOf>(); -expectTypeOf(f.n).toEqualTypeOf(); +expectTypeOf(f.n).toEqualTypeOf>(); expectTypeOf(f.o).toEqualTypeOf<{foo: number }>(); expectTypeOf(f.p).toEqualTypeOf(); expectTypeOf(f.q).toEqualTypeOf>(); @@ -228,24 +234,125 @@ expectTypeOf(f.t).toEqualTypeOf>(); #### With primitive values +Template-only: +- return a [`Cell`](https://github.com/emberjs/rfcs/pull/1071) + ```gjs - +import { reactive } from '@ember/reactive'; + +const exclaim = (cell) => cell.current += '!'; + + ``` +Class-based: +- creates a [`Cell`](https://github.com/emberjs/rfcs/pull/1071), since only decorators can intercept read/writes to primitive values + +```gjs +import Component from '@glimmer/component'; +import { reactive } from '@ember/reactive'; + +export class Demo extends Component { + count = reactive(0); + + increment() { + this.count.current++; + } + + +} +``` + + #### Decorator + primitive value +Class based example, functionally the exact same as using `@tracked` today: + +```gjs +import Component from '@glimmer/component'; +import { reactive } from '@ember/reactive'; + +export class Demo extends Component { + @reactive count = 0; + + increment() { + this.count++; + } + + +} +``` + #### With the _common collections_[^the-common-collections] +Template-only: +- return an object matching the API and prototype of the passed in value + +```gjs +import { reactive } from '@ember/reactive'; + +const initialData = { greeting: 'hello' }; +const exclaim = (data) => data.greeting += '!'; + +``` + +Class-based: +- return an object matching the API and prototype of the passed in value + +```gjs +import Component from '@glimmer/component'; +import { reactive } from '@ember/reactive'; + +export class Demo extends Component { + data = reactive({ greeting: 'hello' }); + + exclaim() { + this.data.greeting += '!'; + } + + +} +``` ### Behavior #### What does reading do? +Starting with a `{{ }}` block, we start reading values. + +when reading a value created by `reactive`, (property on an object, item in a collection, "the value" (of a primitive, via current or via decorator)): +- entangle with the `{{ }}` (keep track of which reactive properties were accessed) + #### What does setting do? +- if a `{{ }}` block had previously entangled with a reactive property, the `{{ }}` will re-render. + ### What are the performance concerns? +TODO + + +## How we teach this + + + + #### With primitive values Template-only: @@ -346,63 +405,6 @@ export class Demo extends Component { } ``` -### Behavior - -#### What does reading do? - -Starting with a `{{ }}` block, we start reading values. - -when reading a value created by `reactive`, (property on an object, item in a collection, "the value" (of a primitive, via current or via decorator)): -- entangle with the `{{ }}` (keep track of which reactive properties were accessed) - -#### What does setting do? - -- if a `{{ }}` block had previously entangled with a reactive property, the `{{ }}` will re-render. - -### What are the performance concerns? - -TODO - - - -## How we teach this - - ## Drawbacks From 97dc967c2e1eae5eecec800977ade8f4785af153 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Fri, 7 Mar 2025 14:24:30 -0500 Subject: [PATCH 07/16] begin shallow planning --- text/1079-ember-reactive.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/text/1079-ember-reactive.md b/text/1079-ember-reactive.md index 340ee86f56..77659874af 100644 --- a/text/1079-ember-reactive.md +++ b/text/1079-ember-reactive.md @@ -300,6 +300,9 @@ users? --> +### Deep tracking + +This type of tracking reactivity will lazily instrument all nested collections[^the-common-collections] #### With primitive values @@ -405,6 +408,9 @@ export class Demo extends Component { } ``` +### Shallow Tracking + +This type of tracking reactivity will intrument one level of the common collection[^the-common-collections] ## Drawbacks From b9d905994a57ab5c591fa40bdeeea012763abc54 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Fri, 7 Mar 2025 14:39:07 -0500 Subject: [PATCH 08/16] Shallow --- text/1079-ember-reactive.md | 41 +++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/text/1079-ember-reactive.md b/text/1079-ember-reactive.md index 77659874af..f44c32a89b 100644 --- a/text/1079-ember-reactive.md +++ b/text/1079-ember-reactive.md @@ -412,6 +412,47 @@ export class Demo extends Component { This type of tracking reactivity will intrument one level of the common collection[^the-common-collections] +#### With the _common collections_[^the-common-collections] + +Template-only: +- return an object matching the API and prototype of the passed in value + +```gjs +import { reactive } from '@ember/reactive'; + +const initialData = { greeting: 'hello' }; +const exclaim = (data) => data.greeting += '!'; + + +``` + +Class-based: +- return an object matching the API and prototype of the passed in value + +```gjs +import Component from '@glimmer/component'; +import { reactive } from '@ember/reactive'; + +export class Demo extends Component { + data = reactive.shallow({ greeting: 'hello' }); + + exclaim() { + this.data.greeting += '!'; + } + + +} +``` + ## Drawbacks From 0d92b0975ad033f31a0fddd05f4b6d0779b2f156 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Fri, 7 Mar 2025 14:45:51 -0500 Subject: [PATCH 09/16] typo --- text/1079-ember-reactive.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/text/1079-ember-reactive.md b/text/1079-ember-reactive.md index f44c32a89b..7fe0096fd3 100644 --- a/text/1079-ember-reactive.md +++ b/text/1079-ember-reactive.md @@ -109,8 +109,6 @@ Feedback on prior RFCs, [Tracking utilities for promises](https://github.com/emb The main tradeoff with deep reactivity vs explicit / shallow is the impact to memory. This RFC describes those tradeoffs, and provides a way for folks to determine when they may want to consider -This doesn't eliminate or remove any existing APIs, but, being a low-level implementation would - This RFC could, but is not required to, replace: - [RFC#1068](https://github.com/emberjs/rfcs/pull/1068): tracked-built-ins, built-in From 66f33a5de4c96c08b0838a16b63bdfdd9ee96f0d Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Fri, 7 Mar 2025 14:51:25 -0500 Subject: [PATCH 10/16] make initial template-only example more minimal --- text/1079-ember-reactive.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/text/1079-ember-reactive.md b/text/1079-ember-reactive.md index 7fe0096fd3..54e3540d54 100644 --- a/text/1079-ember-reactive.md +++ b/text/1079-ember-reactive.md @@ -56,14 +56,12 @@ Example[^example-requires-vm-work]: ```gjs import { reactive } from '@ember/reactive'; -const startingData = { greeting: 'hi' }; -const exclaim = (data) => data.greeting += '!'; +const data = reactive({ greeting: 'hi' }); +const exclaim = () => data.greeting += '!'; export const Demo = ; ``` From e98a7732635fdbbade25a6f8e7a47f1bc1673089 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Fri, 7 Mar 2025 14:51:38 -0500 Subject: [PATCH 11/16] make initial template-only example more minimal --- text/1079-ember-reactive.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/1079-ember-reactive.md b/text/1079-ember-reactive.md index 54e3540d54..f7899d00f6 100644 --- a/text/1079-ember-reactive.md +++ b/text/1079-ember-reactive.md @@ -61,7 +61,7 @@ const exclaim = () => data.greeting += '!'; export const Demo = ; ``` From 97d6ebb414d0205e0bd624a9136201e7a2f60e97 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Fri, 7 Mar 2025 14:57:54 -0500 Subject: [PATCH 12/16] Flip deep and shallow --- text/1079-ember-reactive.md | 98 +++++++++++++++++++------------------ 1 file changed, 50 insertions(+), 48 deletions(-) diff --git a/text/1079-ember-reactive.md b/text/1079-ember-reactive.md index f7899d00f6..f7b4b79deb 100644 --- a/text/1079-ember-reactive.md +++ b/text/1079-ember-reactive.md @@ -115,8 +115,8 @@ This RFC could, but is not required to, replace: Goals for `reactive`: - decorator and non-decorator usage (compatible with typescript) - handles both primitive values and the _common collections_[^the-common-collections] -- deeply reactive[^deep-reactivity] by default (and lazily deeply reactive) -- Since deep reactivity has a memory cost, we also want a shallow version which doesn't infinitely proxy to any depth +- shallow by default (current behavior in existing tools) +- optional deeply reactive[^deep-reactivity] (and lazily deeply reactive) [^the-common-collections]: The comment collection data structures are: `Array`, `Set`, `WeakSet`, `Object`, `Map`, `WeakMap` [^deep-reactivity]: this is to make working with nested data easier, and reduce bugs encountered when updating _parts_ of state. The main known cost is to memory, as we have to make use of proxies around every value so we can continue to be deeply reactive, and lazily reactive. @@ -296,9 +296,51 @@ users? --> -### Deep tracking +### Shallow Tracking + +This type of tracking reactivity will intrument one level of the common collection[^the-common-collections] + +#### With the _common collections_[^the-common-collections] + +Template-only: +- return an object matching the API and prototype of the passed in value + +```gjs +import { reactive } from '@ember/reactive'; + +const initialData = { greeting: 'hello' }; +const exclaim = (data) => data.greeting += '!'; + + +``` + +Class-based: +- return an object matching the API and prototype of the passed in value + +```gjs +import Component from '@glimmer/component'; +import { reactive } from '@ember/reactive'; + +export class Demo extends Component { + data = reactive({ greeting: 'hello' }); + + exclaim() { + this.data.greeting += '!'; + } + + +} +``` -This type of tracking reactivity will lazily instrument all nested collections[^the-common-collections] #### With primitive values @@ -363,50 +405,10 @@ export class Demo extends Component { } ``` -#### With the _common collections_[^the-common-collections] - -Template-only: -- return an object matching the API and prototype of the passed in value - -```gjs -import { reactive } from '@ember/reactive'; - -const initialData = { greeting: 'hello' }; -const exclaim = (data) => data.greeting += '!'; - - -``` - -Class-based: -- return an object matching the API and prototype of the passed in value -```gjs -import Component from '@glimmer/component'; -import { reactive } from '@ember/reactive'; - -export class Demo extends Component { - data = reactive({ greeting: 'hello' }); - - exclaim() { - this.data.greeting += '!'; - } - - -} -``` - -### Shallow Tracking +### Deep tracking -This type of tracking reactivity will intrument one level of the common collection[^the-common-collections] +This type of tracking reactivity will lazily instrument all nested collections[^the-common-collections] #### With the _common collections_[^the-common-collections] @@ -420,7 +422,7 @@ const initialData = { greeting: 'hello' }; const exclaim = (data) => data.greeting += '!';