diff --git a/documentation/docs/02-runes/02-$state.md b/documentation/docs/02-runes/02-$state.md
index e8213d3cf4a8..eab3c1f0af3b 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,165 @@ 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`, `$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
+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
+// @errors: 7006
+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
+// @errors: 7006
+function createTodo(initial) {
+ const todo = { text: initial, done: false }
+ return {
+ todo,
+ log: () => console.log(todo.text, todo.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
+// @errors: 7006
+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`), 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)
+ 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
+// @errors: 7006
+/// 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
+// @errors: 7006
+/// 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
+counter.increment(); // $effect logs 1
+```
+
+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
+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`:
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 }