Skip to content

Commit 1809747

Browse files
trueadmRich-Harris
andauthored
breaking: remove unstate(), replace with $state.snapshot rune (#11180)
* breaking: remove untrack(), replace with $state.clean rune * lol * update types * update types * undo * undo * rename to raw * rename to snapshot * fix * tweak docs, to make it explicitly that we're converting to and from proxies * remove vestiges * validation * tweak --------- Co-authored-by: Rich Harris <[email protected]>
1 parent cd7c3fe commit 1809747

File tree

20 files changed

+117
-59
lines changed

20 files changed

+117
-59
lines changed

.changeset/clever-sloths-push.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"svelte": patch
3+
---
4+
5+
breaking: remove unstate(), replace with $state.snapshot rune

packages/svelte/src/ambient.d.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,26 @@ declare namespace $state {
4242
*/
4343
export function frozen<T>(initial: T): Readonly<T>;
4444
export function frozen<T>(): Readonly<T> | undefined;
45+
/**
46+
* To take a static snapshot of a deeply reactive `$state` proxy, use `$state.snapshot`:
47+
*
48+
* Example:
49+
* ```ts
50+
* <script>
51+
* let counter = $state({ count: 0 });
52+
*
53+
* function onclick() {
54+
* // Will log `{ count: ... }` rather than `Proxy { ... }`
55+
* console.log($state.snapshot(counter));
56+
* };
57+
* </script>
58+
* ```
59+
*
60+
* https://svelte-5-preview.vercel.app/docs/runes#$state.snapshot
61+
*
62+
* @param state The value to snapshot
63+
*/
64+
export function snapshot<T>(state: T): T;
4565
}
4666

4767
/**

packages/svelte/src/compiler/phases/2-analyze/validation.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -865,6 +865,12 @@ function validate_call_expression(node, scope, path) {
865865
error(node, 'invalid-rune-args-length', rune, [1]);
866866
}
867867
}
868+
869+
if (rune === '$state.snapshot') {
870+
if (node.arguments.length !== 1) {
871+
error(node, 'invalid-rune-args-length', rune, [1]);
872+
}
873+
}
868874
}
869875

870876
/**

packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,13 @@ export const javascript_visitors_runes = {
388388
return b.call('$.effect_active');
389389
}
390390

391+
if (rune === '$state.snapshot') {
392+
return b.call(
393+
'$.snapshot',
394+
/** @type {import('estree').Expression} */ (context.visit(node.arguments[0]))
395+
);
396+
}
397+
391398
if (rune === '$effect.root') {
392399
const args = /** @type {import('estree').Expression[]} */ (
393400
node.arguments.map((arg) => context.visit(arg))

packages/svelte/src/compiler/phases/3-transform/server/transform-server.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -794,6 +794,10 @@ const javascript_visitors_runes = {
794794
return b.literal(false);
795795
}
796796

797+
if (rune === '$state.snapshot') {
798+
return /** @type {import('estree').Expression} */ (context.visit(node.arguments[0]));
799+
}
800+
797801
if (rune === '$inspect' || rune === '$inspect().with') {
798802
return transform_inspect_rune(node, context);
799803
}

packages/svelte/src/compiler/phases/constants.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export const PassiveEvents = ['wheel', 'touchstart', 'touchmove', 'touchend', 't
3131
export const Runes = /** @type {const} */ ([
3232
'$state',
3333
'$state.frozen',
34+
'$state.snapshot',
3435
'$props',
3536
'$bindable',
3637
'$derived',

packages/svelte/src/index-client.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -177,8 +177,6 @@ export function flushSync(fn) {
177177
flush_sync(fn);
178178
}
179179

180-
export { unstate } from './internal/client/proxy.js';
181-
182180
export { hydrate, mount, unmount } from './internal/client/render.js';
183181

184182
export {

packages/svelte/src/index-server.js

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,4 @@ export function unmount() {
3333

3434
export async function tick() {}
3535

36-
/**
37-
* @template T
38-
* @param {T} value
39-
* @returns {T}
40-
*/
41-
export function unstate(value) {
42-
// There's no signals/proxies on the server, so just return the value
43-
return value;
44-
}
45-
4636
export { getAllContexts, getContext, hasContext, setContext } from './internal/server/context.js';

packages/svelte/src/internal/client/dom/blocks/each.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import {
2323
resume_effect
2424
} from '../../reactivity/effects.js';
2525
import { source, mutable_source, set } from '../../reactivity/sources.js';
26-
import { is_array, is_frozen, map_get, map_set } from '../../utils.js';
26+
import { is_array, is_frozen } from '../../utils.js';
2727
import { STATE_SYMBOL } from '../../constants.js';
2828

2929
/**

packages/svelte/src/internal/client/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ export {
128128
validate_store
129129
} from './validate.js';
130130
export { raf } from './timing.js';
131-
export { proxy, unstate } from './proxy.js';
131+
export { proxy, snapshot } from './proxy.js';
132132
export { create_custom_element } from './dom/elements/custom-element.js';
133133
export {
134134
child,

packages/svelte/src/internal/client/proxy.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ function unwrap(value, already_unwrapped) {
140140
* @param {T} value
141141
* @returns {T}
142142
*/
143-
export function unstate(value) {
143+
export function snapshot(value) {
144144
return /** @type {T} */ (
145145
unwrap(/** @type {import('#client').ProxyStateObject} */ (value), new Map())
146146
);

packages/svelte/src/internal/client/runtime.js

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
object_freeze,
88
object_prototype
99
} from './utils.js';
10-
import { unstate } from './proxy.js';
10+
import { snapshot } from './proxy.js';
1111
import { destroy_effect, effect, user_pre_effect } from './reactivity/effects.js';
1212
import {
1313
EFFECT,
@@ -1176,26 +1176,26 @@ export function deep_read(value, visited = new Set()) {
11761176
}
11771177

11781178
/**
1179-
* Like `unstate`, but recursively traverses into normal arrays/objects to find potential states in them.
1179+
* Like `snapshot`, but recursively traverses into normal arrays/objects to find potential states in them.
11801180
* @param {any} value
11811181
* @param {Map<any, any>} visited
11821182
* @returns {any}
11831183
*/
1184-
function deep_unstate(value, visited = new Map()) {
1184+
function deep_snapshot(value, visited = new Map()) {
11851185
if (typeof value === 'object' && value !== null && !visited.has(value)) {
1186-
const unstated = unstate(value);
1186+
const unstated = snapshot(value);
11871187
if (unstated !== value) {
11881188
visited.set(value, unstated);
11891189
return unstated;
11901190
}
11911191
const prototype = get_prototype_of(value);
1192-
// Only deeply unstate plain objects and arrays
1192+
// Only deeply snapshot plain objects and arrays
11931193
if (prototype === object_prototype || prototype === array_prototype) {
11941194
let contains_unstated = false;
11951195
/** @type {any} */
11961196
const nested_unstated = Array.isArray(value) ? [] : {};
11971197
for (let key in value) {
1198-
const result = deep_unstate(value[key], visited);
1198+
const result = deep_snapshot(value[key], visited);
11991199
nested_unstated[key] = result;
12001200
if (result !== value[key]) {
12011201
contains_unstated = true;
@@ -1223,7 +1223,7 @@ export function inspect(get_value, inspect = console.log) {
12231223

12241224
user_pre_effect(() => {
12251225
const fn = () => {
1226-
const value = untrack(() => get_value().map((v) => deep_unstate(v)));
1226+
const value = untrack(() => get_value().map((v) => deep_snapshot(v)));
12271227
if (value.length === 2 && typeof value[1] === 'function' && !warned_inspect_changed) {
12281228
// eslint-disable-next-line no-console
12291229
console.warn(
@@ -1311,9 +1311,9 @@ if (DEV) {
13111311
*/
13121312
export function freeze(value) {
13131313
if (typeof value === 'object' && value != null && !is_frozen(value)) {
1314-
// If the object is already proxified, then unstate the value
1314+
// If the object is already proxified, then snapshot the value
13151315
if (STATE_SYMBOL in value) {
1316-
return object_freeze(unstate(value));
1316+
return object_freeze(snapshot(value));
13171317
}
13181318
// Otherwise freeze the object
13191319
object_freeze(value);
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
error: {
5+
code: 'invalid-rune-args-length',
6+
message: '$state.snapshot can only be called with 1 argument'
7+
}
8+
});
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<script>
2+
const foo = $state.snapshot();
3+
</script>
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
<script>
2-
import { unstate } from 'svelte';
3-
42
let items = $state([{a: 0}]);
53
</script>
64

7-
<button on:click={() => items.push({a: items.length})}>{JSON.stringify(structuredClone(unstate(items)))}</button>
5+
<button on:click={() => items.push({a: items.length})}>{JSON.stringify(structuredClone($state.snapshot(items)))}</button>

packages/svelte/types/index.d.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -297,7 +297,6 @@ declare module 'svelte' {
297297
export function flushSync(fn?: (() => void) | undefined): void;
298298
/** Anything except a function */
299299
type NotFunction<T> = T extends Function ? never : T;
300-
export function unstate<T>(value: T): T;
301300
/**
302301
* Mounts a component to the given target and returns the exports and potentially the props (if compiled with `accessors: true`) of the component
303302
*
@@ -2551,6 +2550,26 @@ declare namespace $state {
25512550
*/
25522551
export function frozen<T>(initial: T): Readonly<T>;
25532552
export function frozen<T>(): Readonly<T> | undefined;
2553+
/**
2554+
* To take a static snapshot of a deeply reactive `$state` proxy, use `$state.snapshot`:
2555+
*
2556+
* Example:
2557+
* ```ts
2558+
* <script>
2559+
* let counter = $state({ count: 0 });
2560+
*
2561+
* function onclick() {
2562+
* // Will log `{ count: ... }` rather than `Proxy { ... }`
2563+
* console.log($state.snapshot(counter));
2564+
* };
2565+
* </script>
2566+
* ```
2567+
*
2568+
* https://svelte-5-preview.vercel.app/docs/runes#$state.snapshot
2569+
*
2570+
* @param state The value to snapshot
2571+
*/
2572+
export function snapshot<T>(state: T): T;
25542573
}
25552574

25562575
/**

sites/svelte-5-preview/src/lib/CodeMirror.svelte

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -205,27 +205,28 @@
205205
return {
206206
from: word.from - 1,
207207
options: [
208-
{ label: '$state', type: 'keyword', boost: 10 },
209-
{ label: '$props', type: 'keyword', boost: 9 },
210-
{ label: '$derived', type: 'keyword', boost: 8 },
208+
{ label: '$state', type: 'keyword', boost: 12 },
209+
{ label: '$props', type: 'keyword', boost: 11 },
210+
{ label: '$derived', type: 'keyword', boost: 10 },
211211
snip('$derived.by(() => {\n\t${}\n});', {
212212
label: '$derived.by',
213213
type: 'keyword',
214-
boost: 7
214+
boost: 9
215215
}),
216-
snip('$effect(() => {\n\t${}\n});', { label: '$effect', type: 'keyword', boost: 6 }),
216+
snip('$effect(() => {\n\t${}\n});', { label: '$effect', type: 'keyword', boost: 8 }),
217217
snip('$effect.pre(() => {\n\t${}\n});', {
218218
label: '$effect.pre',
219219
type: 'keyword',
220-
boost: 5
220+
boost: 7
221221
}),
222-
{ label: '$state.frozen', type: 'keyword', boost: 4 },
223-
{ label: '$bindable', type: 'keyword', boost: 4 },
222+
{ label: '$state.frozen', type: 'keyword', boost: 6 },
223+
{ label: '$bindable', type: 'keyword', boost: 5 },
224224
snip('$effect.root(() => {\n\t${}\n});', {
225225
label: '$effect.root',
226226
type: 'keyword',
227-
boost: 3
227+
boost: 4
228228
}),
229+
{ label: '$state.snapshot', type: 'keyword', boost: 3 },
229230
snip('$effect.active()', {
230231
label: '$effect.active',
231232
type: 'keyword',

sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ class Todo {
4040

4141
> In this example, the compiler transforms `done` and `text` into `get`/`set` methods on the class prototype referencing private fields
4242
43-
Objects and arrays [are made deeply reactive](/#H4sIAAAAAAAAE42QwWrDMBBEf2URhUhUNEl7c21DviPOwZY3jVpZEtIqUBz9e-UUt9BTj7M784bdmZ21wciq48xsPyGr2MF7Jhl9-kXEKxrCoqNLQS2TOqqgPbWd7cgggU3TgCFCAw-RekJ-3Et4lvByEq-drbe_dlsPichZcFYZrT6amQto2pXw5FO88FUYtG90gUfYi3zvWrYL75vxL57zfA07_zfr23k1vjtt-aZ0bQTcbrDL5ZifZcAxKeS8lzDc8X0xDhJ2ItdbX1jlOZMb9VnjyCoKCfMpfwG975NFVwEAAA==):
43+
Objects and arrays [are made deeply reactive](/#H4sIAAAAAAAAE42QwWrDMBBEf2URhUhUNEl7c21DviPOwZY3jVpZEtIqUBz9e-UUt9BTj7M784bdmZ21wciq48xsPyGr2MF7Jhl9-kXEKxrCoqNLQS2TOqqgPbWd7cgggU3TgCFCAw-RekJ-3Et4lvByEq-drbe_dlsPichZcFYZrT6amQto2pXw5FO88FUYtG90gUfYi3zvWrYL75vxL57zfA07_zfr23k1vjtt-aZ0bQTcbrDL5ZifZcAxKeS8lzDc8X0xDhJ2ItdbX1jlOZMb9VnjyCoKCfMpfwG975NFVwEAAA==) by wrapping them with [`Proxies`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy):
4444

4545
```svelte
4646
<script>
@@ -112,6 +112,25 @@ Svelte provides reactive `Map`, `Set` and `Date` classes. These can be imported
112112
<p>{map.get('message')}</p>
113113
```
114114

115+
## `$state.snapshot`
116+
117+
To take a static snapshot of a deeply reactive `$state` proxy, use `$state.snapshot`:
118+
119+
```svelte
120+
<script>
121+
let counter = $state({ count: 0 });
122+
123+
function onclick() {
124+
// Will log `{ count: ... }` rather than `Proxy { ... }`
125+
console.log($state.snapshot(counter));
126+
}
127+
</script>
128+
```
129+
130+
This is handy when you want to pass some state to an external library or API that doesn't expect a proxy, such as `structuredClone`.
131+
132+
> Note that `$state.snapshot` will clone the data when removing reactivity. If the value passed isn't a `$state` proxy, it will be returned as-is.
133+
115134
## `$derived`
116135

117136
Derived state is declared with the `$derived` rune:

sites/svelte-5-preview/src/routes/docs/content/01-api/05-functions.md

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -23,27 +23,6 @@ To prevent something from being treated as an `$effect`/`$derived` dependency, u
2323
</script>
2424
```
2525

26-
## `unstate`
27-
28-
To remove reactivity from objects and arrays created with `$state`, use `unstate`:
29-
30-
```svelte
31-
<script>
32-
import { unstate } from 'svelte';
33-
34-
let counter = $state({ count: 0 });
35-
36-
$effect(() => {
37-
// Will log { count: 0 }
38-
console.log(unstate(counter));
39-
});
40-
</script>
41-
```
42-
43-
This is handy when you want to pass some state to an external library or API that doesn't expect a reactive object – such as `structuredClone`.
44-
45-
> Note that `unstate` will return a new object from the input when removing reactivity. If the object passed isn't reactive, it will be returned as is.
46-
4726
## `mount`
4827

4928
Instantiates a component and mounts it to the given target:

0 commit comments

Comments
 (0)