From 7bcbf9dc8e400a9812f773bd09d2a43c1d4cdace Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Fri, 15 Nov 2024 11:49:24 +0100 Subject: [PATCH 1/6] docs: explain how to pass reactivity across boundaries closes #14300 closes #14261 --- documentation/docs/02-runes/02-$state.md | 160 +++++++++++++++++++- documentation/docs/02-runes/03-$derived.md | 4 + documentation/docs/06-runtime/02-context.md | 4 +- 3 files changed, 165 insertions(+), 3 deletions(-) diff --git a/documentation/docs/02-runes/02-$state.md b/documentation/docs/02-runes/02-$state.md index e8213d3cf4a8..0341f090b28c 100644 --- a/documentation/docs/02-runes/02-$state.md +++ b/documentation/docs/02-runes/02-$state.md @@ -64,9 +64,35 @@ todos.push({ > [!NOTE] When you update properties of proxies, the original object is _not_ mutated. +Since `$state` stops at boundaries that are not simple arrays or objects, the following will not trigger any reactivity: + +```svelte + + + +``` + +You can however use `$state` _inside_ the class to make it work, as explained in the next section. + ### Classes -You can also use `$state` in class fields (whether public or private): +You can use `$state` in class fields (whether public or private): ```js // @errors: 7006 2554 @@ -85,7 +111,7 @@ class Todo { } ``` -> [!NOTE] The compiler transforms `done` and `text` into `get`/`set` methods on the class prototype referencing private fields. +Under the hood, the compiler transforms `done` and `text` into `get`/`set` methods on the class prototype referencing private fields. That means the properties are _not_ enumerable. ## `$state.raw` @@ -111,6 +137,136 @@ person = { This can improve performance with large arrays and objects that you weren't planning to mutate anyway, since it avoids the cost of making them reactive. Note that raw state can _contain_ reactive state (for example, a raw array of reactive objects). +## Passing `$state` across boundaries + +Since there's no wrapper around `$state` or `$state.raw` (or [`$derived`]($derived)), you have to be aware of keeping reactivity alive when passing it across boundaries, e.g. when you pass a reactive object into or out of a function. The most succinct way of thinking about this is to treat `$state` and `$state.raw` (and [`$derived`]($derived)) as "just JavaScript", and reuse the knowledge of how normal JavaScript variables work when crossing boundaries. Take the following example: + +```js +function createTodo(initial) { + let text = initial; + let done = false; + return { + text, + done, + log: () => console.log(text, done) + } +} + +const todo = createTodo('wrong'); +todo.log(); // logs "'wrong', false" +todo.done = true; +todo.log(); // still logs "'wrong', false" +``` + +The value change does not propagate back into the function body of `createTodo`, because `text` and `done` are read at the point of return, and are therefore a fixed value. To make that work, we have to bring the read and write into the scope of the function body. This can be done via getter/setters or via function calls: + +```js +function createTodo(initial) { + let text = initial; + let done = false; + return { + // using getter/setter + get text() { return text }, + set text(v) { text = v }, + // using functions + isDone() { return done }, + toggle() { done = !done }, + // log + log: () => console.log(text, done) + } +} + +const todo = createTodo('right'); +todo.log(); // logs "'right', false" +todo.text = 'changed'; // invokes the setter +todo.toggle(); // invokes the function +todo.log(); // logs "'changed', true" +``` + +What you could also do is to instead create an object and return that as a whole. While the variable itself is fixed in time, its properties are not, and so they can be changed from the outside and the changes are observable from within the function: + +```js +function createTodo(initial) { + const todo = { text: initial, done: false } + return { + todo, + log: () => console.log(text, done) + } +} + +const todo = createTodo('right'); +todo.log(); // logs "'right', false" +todo.todo.done = true; // mutates the object +todo.log(); // logs "'right', true" +``` + +Classes are similar, their properties are "live" due to the `this` context: + +```js +class Todo { + done = false; + text; + + constructor(text) { + this.text = text; + } + + log() { + console.log(this.done, this.text) + } +} + +const todo = new Todo('right'); +todo.log(); // logs "'right', false" +todo.done = true; +todo.log(); // logs "'right', true" +``` + +Notice how we didn't use _any_ Svelte specifics, this is just regular JavaScript semantics. `$state` and `$state.raw` (and [`$derived`]($derived)) don't change these, they just add reactivity on top, so that when you change a variable something can happen in reaction to it. + +As a consequence, the answer to preserving reactivity across boundaries is to use getters/setters or functions (in case of `$state`, `$state.raw` and `$derived`) or an object with mutable properties (in case of `$state`), or a class with reactive properties. + +```js +/// file: getters-setters-functions.svelte.js +function doubler(count) { + const double = $derived(count() * 2) + return { + get current() { return double } + }; +} + +let count = $state(0); +const double = doubler(() => count); +$effect(() => console.log(double.current)); // $effect logs 0 +count = 1; // $effect logs 2 +``` + +```js +/// file: mutable-object.svelte.js +function logger(value) { + $effect(() => console.log(value.current)); +} + +let count = $state({ current: 0 }); +logger(count); // $effect logs 0 +count.current = 1; // $effect logs 1 +``` + +```js +/// file: class.svelte.js +function logger(counter) { + $effect(() => console.log(counter.count)); +} + +class Counter { + count = $state(0); + increment() { this.count++; } +} +let counter = new Counter(); +logger(counter); // $effect logs 0 +count.increment(); // $effect logs 1 +``` + ## `$state.snapshot` To take a static snapshot of a deeply reactive `$state` proxy, use `$state.snapshot`: diff --git a/documentation/docs/02-runes/03-$derived.md b/documentation/docs/02-runes/03-$derived.md index 6b38f9974672..90b43c66e676 100644 --- a/documentation/docs/02-runes/03-$derived.md +++ b/documentation/docs/02-runes/03-$derived.md @@ -51,3 +51,7 @@ In essence, `$derived(expression)` is equivalent to `$derived.by(() => expressio Anything read synchronously inside the `$derived` expression (or `$derived.by` function body) is considered a _dependency_ of the derived state. When the state changes, the derived will be marked as _dirty_ and recalculated when it is next read. To exempt a piece of state from being treated as a dependency, use [`untrack`](svelte#untrack). + +## Passing `$derived` across boundaries + +The same rules as for [passing `$state` across boundaries]($state#Passing-$state-across-boundaries) apply. diff --git a/documentation/docs/06-runtime/02-context.md b/documentation/docs/06-runtime/02-context.md index 62dd0c6a9e7b..ba782efaaf61 100644 --- a/documentation/docs/06-runtime/02-context.md +++ b/documentation/docs/06-runtime/02-context.md @@ -63,7 +63,7 @@ The context is then available to children of the component (including slotted co > [!NOTE] `setContext`/`getContext` must be called during component initialisation. -Context is not inherently reactive. If you need reactive values in context then you can pass a `$state` object into context, whose properties _will_ be reactive. +Context is not inherently reactive. If you need reactive values in context then you can pass a `$state` object into context, whose properties _will_ be reactive (for more info see ["passing `$state` across boundaries"]($state#Passing-$state-across-boundaries)). ```svelte @@ -72,6 +72,8 @@ Context is not inherently reactive. If you need reactive values in context then let value = $state({ count: 0 }); setContext('counter', value); + // careful: reassignments will _not_ change the value inside context: + // value = { count: 0 } From 543e455f50b1cca06bf44bec8025813135bfc939 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Fri, 15 Nov 2024 12:05:00 +0100 Subject: [PATCH 2/6] fix --- documentation/docs/02-runes/02-$state.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/documentation/docs/02-runes/02-$state.md b/documentation/docs/02-runes/02-$state.md index 0341f090b28c..d948d624642a 100644 --- a/documentation/docs/02-runes/02-$state.md +++ b/documentation/docs/02-runes/02-$state.md @@ -142,6 +142,7 @@ This can improve performance with large arrays and objects that you weren't plan Since there's no wrapper around `$state` or `$state.raw` (or [`$derived`]($derived)), you have to be aware of keeping reactivity alive when passing it across boundaries, e.g. when you pass a reactive object into or out of a function. The most succinct way of thinking about this is to treat `$state` and `$state.raw` (and [`$derived`]($derived)) as "just JavaScript", and reuse the knowledge of how normal JavaScript variables work when crossing boundaries. Take the following example: ```js +// @errors: 7006 function createTodo(initial) { let text = initial; let done = false; @@ -161,6 +162,7 @@ todo.log(); // still logs "'wrong', false" The value change does not propagate back into the function body of `createTodo`, because `text` and `done` are read at the point of return, and are therefore a fixed value. To make that work, we have to bring the read and write into the scope of the function body. This can be done via getter/setters or via function calls: ```js +// @errors: 7006 function createTodo(initial) { let text = initial; let done = false; @@ -186,11 +188,12 @@ todo.log(); // logs "'changed', true" What you could also do is to instead create an object and return that as a whole. While the variable itself is fixed in time, its properties are not, and so they can be changed from the outside and the changes are observable from within the function: ```js +// @errors: 7006 function createTodo(initial) { const todo = { text: initial, done: false } return { todo, - log: () => console.log(text, done) + log: () => console.log(todo.text, todo.done) } } @@ -203,6 +206,7 @@ todo.log(); // logs "'right', true" Classes are similar, their properties are "live" due to the `this` context: ```js +// @errors: 7006 class Todo { done = false; text; @@ -227,6 +231,7 @@ Notice how we didn't use _any_ Svelte specifics, this is just regular JavaScript As a consequence, the answer to preserving reactivity across boundaries is to use getters/setters or functions (in case of `$state`, `$state.raw` and `$derived`) or an object with mutable properties (in case of `$state`), or a class with reactive properties. ```js +// @errors: 7006 /// file: getters-setters-functions.svelte.js function doubler(count) { const double = $derived(count() * 2) @@ -242,6 +247,7 @@ count = 1; // $effect logs 2 ``` ```js +// @errors: 7006 /// file: mutable-object.svelte.js function logger(value) { $effect(() => console.log(value.current)); @@ -264,7 +270,7 @@ class Counter { } let counter = new Counter(); logger(counter); // $effect logs 0 -count.increment(); // $effect logs 1 +counter.increment(); // $effect logs 1 ``` ## `$state.snapshot` From 11dbd6a0af4370ffdd83ab43db68ecb4f6e6113b Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Fri, 15 Nov 2024 14:11:39 +0100 Subject: [PATCH 3/6] fix; explain destructuring gotcha --- documentation/docs/02-runes/02-$state.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/documentation/docs/02-runes/02-$state.md b/documentation/docs/02-runes/02-$state.md index d948d624642a..5e72db6c3799 100644 --- a/documentation/docs/02-runes/02-$state.md +++ b/documentation/docs/02-runes/02-$state.md @@ -259,6 +259,7 @@ count.current = 1; // $effect logs 1 ``` ```js +// @errors: 7006 /// file: class.svelte.js function logger(counter) { $effect(() => console.log(counter.count)); @@ -273,6 +274,28 @@ logger(counter); // $effect logs 0 counter.increment(); // $effect logs 1 ``` +For the same reasons, you should not destructure reactive objects, because that means their value is read at that point in time, and not updated anymore from inside whatever created it. + +```js +// @errors: 7006 +class Counter { + count = $state(0); + increment = () => { this.count++; } +} + +// don't do this +let { count, increment } = new Counter(); +count; // 0 +increment(); +count; // still 0 + +// do this instead +let counter = new Counter(); +counter.count; // 0 +counter.increment(); +counter.count; // 1 +``` + ## `$state.snapshot` To take a static snapshot of a deeply reactive `$state` proxy, use `$state.snapshot`: From f928779665ba06e369aa0199b0f5c26b58f039fb Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Fri, 22 Nov 2024 22:12:29 +0100 Subject: [PATCH 4/6] Update documentation/docs/02-runes/02-$state.md Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com> --- documentation/docs/02-runes/02-$state.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/docs/02-runes/02-$state.md b/documentation/docs/02-runes/02-$state.md index 5e72db6c3799..d0e39a532954 100644 --- a/documentation/docs/02-runes/02-$state.md +++ b/documentation/docs/02-runes/02-$state.md @@ -139,7 +139,7 @@ This can improve performance with large arrays and objects that you weren't plan ## Passing `$state` across boundaries -Since there's no wrapper around `$state` or `$state.raw` (or [`$derived`]($derived)), you have to be aware of keeping reactivity alive when passing it across boundaries, e.g. when you pass a reactive object into or out of a function. The most succinct way of thinking about this is to treat `$state` and `$state.raw` (and [`$derived`]($derived)) as "just JavaScript", and reuse the knowledge of how normal JavaScript variables work when crossing boundaries. Take the following example: +Since there's no wrapper around `$state`, `$state.raw`, or [`$derived`]($derived), you have to be aware of keeping reactivity alive when passing it across boundaries — e.g. when you pass a reactive object into or out of a function. The most succinct way of thinking about this is to treat `$state`, `$state.raw`, and [`$derived`]($derived) as "just JavaScript", and reuse the knowledge of how normal JavaScript variables work when crossing boundaries. Take the following example: ```js // @errors: 7006 From ff28761ff3fae09f1ce5b1db8c32aca2b631ab3e Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Fri, 22 Nov 2024 22:13:38 +0100 Subject: [PATCH 5/6] Update documentation/docs/02-runes/02-$state.md Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com> --- documentation/docs/02-runes/02-$state.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/docs/02-runes/02-$state.md b/documentation/docs/02-runes/02-$state.md index d0e39a532954..4809258bd93b 100644 --- a/documentation/docs/02-runes/02-$state.md +++ b/documentation/docs/02-runes/02-$state.md @@ -228,7 +228,7 @@ todo.log(); // logs "'right', true" Notice how we didn't use _any_ Svelte specifics, this is just regular JavaScript semantics. `$state` and `$state.raw` (and [`$derived`]($derived)) don't change these, they just add reactivity on top, so that when you change a variable something can happen in reaction to it. -As a consequence, the answer to preserving reactivity across boundaries is to use getters/setters or functions (in case of `$state`, `$state.raw` and `$derived`) or an object with mutable properties (in case of `$state`), or a class with reactive properties. +As a consequence, the answer to preserving reactivity across boundaries is to use getters/setters or functions (in case of `$state`, `$state.raw` and `$derived`), an object with mutable properties (in case of `$state`), or a class with reactive properties. ```js // @errors: 7006 From 3972524f94d7e18c2254ffb56ae5873d6a65dcf1 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Fri, 22 Nov 2024 22:13:59 +0100 Subject: [PATCH 6/6] Update documentation/docs/02-runes/02-$state.md Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com> --- documentation/docs/02-runes/02-$state.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/docs/02-runes/02-$state.md b/documentation/docs/02-runes/02-$state.md index 4809258bd93b..eab3c1f0af3b 100644 --- a/documentation/docs/02-runes/02-$state.md +++ b/documentation/docs/02-runes/02-$state.md @@ -274,7 +274,7 @@ logger(counter); // $effect logs 0 counter.increment(); // $effect logs 1 ``` -For the same reasons, you should not destructure reactive objects, because that means their value is read at that point in time, and not updated anymore from inside whatever created it. +For the same reasons, you should not destructure reactive objects — their value is read at that point in time, and not updated anymore from inside whatever created it. ```js // @errors: 7006