Skip to content

breaking: remove unstate(), replace with $state.snapshot rune #11180

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Apr 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/clever-sloths-push.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"svelte": patch
---

breaking: remove unstate(), replace with $state.snapshot rune
20 changes: 20 additions & 0 deletions packages/svelte/src/ambient.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,26 @@ declare namespace $state {
*/
export function frozen<T>(initial: T): Readonly<T>;
export function frozen<T>(): Readonly<T> | undefined;
/**
* To take a static snapshot of a deeply reactive `$state` proxy, use `$state.snapshot`:
*
* Example:
* ```ts
* <script>
* let counter = $state({ count: 0 });
*
* function onclick() {
* // Will log `{ count: ... }` rather than `Proxy { ... }`
* console.log($state.snapshot(counter));
* };
* </script>
* ```
*
* https://svelte-5-preview.vercel.app/docs/runes#$state.snapshot
*
* @param state The value to snapshot
*/
export function snapshot<T>(state: T): T;
}

/**
Expand Down
6 changes: 6 additions & 0 deletions packages/svelte/src/compiler/phases/2-analyze/validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -865,6 +865,12 @@ function validate_call_expression(node, scope, path) {
error(node, 'invalid-rune-args-length', rune, [1]);
}
}

if (rune === '$state.snapshot') {
if (node.arguments.length !== 1) {
error(node, 'invalid-rune-args-length', rune, [1]);
}
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,13 @@ export const javascript_visitors_runes = {
return b.call('$.effect_active');
}

if (rune === '$state.snapshot') {
return b.call(
'$.snapshot',
/** @type {import('estree').Expression} */ (context.visit(node.arguments[0]))
);
}

if (rune === '$effect.root') {
const args = /** @type {import('estree').Expression[]} */ (
node.arguments.map((arg) => context.visit(arg))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -793,6 +793,10 @@ const javascript_visitors_runes = {
return b.literal(false);
}

if (rune === '$state.snapshot') {
return /** @type {import('estree').Expression} */ (context.visit(node.arguments[0]));
}

if (rune === '$inspect' || rune === '$inspect().with') {
return transform_inspect_rune(node, context);
}
Expand Down
1 change: 1 addition & 0 deletions packages/svelte/src/compiler/phases/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export const PassiveEvents = ['wheel', 'touchstart', 'touchmove', 'touchend', 't
export const Runes = /** @type {const} */ ([
'$state',
'$state.frozen',
'$state.snapshot',
'$props',
'$bindable',
'$derived',
Expand Down
2 changes: 0 additions & 2 deletions packages/svelte/src/index-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -177,8 +177,6 @@ export function flushSync(fn) {
flush_sync(fn);
}

export { unstate } from './internal/client/proxy.js';

export { hydrate, mount, unmount } from './internal/client/render.js';

export {
Expand Down
10 changes: 0 additions & 10 deletions packages/svelte/src/index-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,4 @@ export function unmount() {

export async function tick() {}

/**
* @template T
* @param {T} value
* @returns {T}
*/
export function unstate(value) {
// There's no signals/proxies on the server, so just return the value
return value;
}

export { getAllContexts, getContext, hasContext, setContext } from './internal/server/context.js';
2 changes: 1 addition & 1 deletion packages/svelte/src/internal/client/dom/blocks/each.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {
resume_effect
} from '../../reactivity/effects.js';
import { source, mutable_source, set } from '../../reactivity/sources.js';
import { is_array, is_frozen, map_get, map_set } from '../../utils.js';
import { is_array, is_frozen } from '../../utils.js';
import { STATE_SYMBOL } from '../../constants.js';

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/svelte/src/internal/client/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ export {
validate_store
} from './validate.js';
export { raf } from './timing.js';
export { proxy, unstate } from './proxy.js';
export { proxy, snapshot } from './proxy.js';
export { create_custom_element } from './dom/elements/custom-element.js';
export {
child,
Expand Down
2 changes: 1 addition & 1 deletion packages/svelte/src/internal/client/proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ function unwrap(value, already_unwrapped) {
* @param {T} value
* @returns {T}
*/
export function unstate(value) {
export function snapshot(value) {
return /** @type {T} */ (
unwrap(/** @type {import('#client').ProxyStateObject} */ (value), new Map())
);
Expand Down
18 changes: 9 additions & 9 deletions packages/svelte/src/internal/client/runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
object_freeze,
object_prototype
} from './utils.js';
import { unstate } from './proxy.js';
import { snapshot } from './proxy.js';
import { destroy_effect, effect, user_pre_effect } from './reactivity/effects.js';
import {
EFFECT,
Expand Down Expand Up @@ -1166,26 +1166,26 @@ export function deep_read(value, visited = new Set()) {
}

/**
* Like `unstate`, but recursively traverses into normal arrays/objects to find potential states in them.
* Like `snapshot`, but recursively traverses into normal arrays/objects to find potential states in them.
* @param {any} value
* @param {Map<any, any>} visited
* @returns {any}
*/
function deep_unstate(value, visited = new Map()) {
function deep_snapshot(value, visited = new Map()) {
if (typeof value === 'object' && value !== null && !visited.has(value)) {
const unstated = unstate(value);
const unstated = snapshot(value);
if (unstated !== value) {
visited.set(value, unstated);
return unstated;
}
const prototype = get_prototype_of(value);
// Only deeply unstate plain objects and arrays
// Only deeply snapshot plain objects and arrays
if (prototype === object_prototype || prototype === array_prototype) {
let contains_unstated = false;
/** @type {any} */
const nested_unstated = Array.isArray(value) ? [] : {};
for (let key in value) {
const result = deep_unstate(value[key], visited);
const result = deep_snapshot(value[key], visited);
nested_unstated[key] = result;
if (result !== value[key]) {
contains_unstated = true;
Expand Down Expand Up @@ -1213,7 +1213,7 @@ export function inspect(get_value, inspect = console.log) {

user_pre_effect(() => {
const fn = () => {
const value = untrack(() => get_value().map((v) => deep_unstate(v)));
const value = untrack(() => get_value().map((v) => deep_snapshot(v)));
if (value.length === 2 && typeof value[1] === 'function' && !warned_inspect_changed) {
// eslint-disable-next-line no-console
console.warn(
Expand Down Expand Up @@ -1301,9 +1301,9 @@ if (DEV) {
*/
export function freeze(value) {
if (typeof value === 'object' && value != null && !is_frozen(value)) {
// If the object is already proxified, then unstate the value
// If the object is already proxified, then snapshot the value
if (STATE_SYMBOL in value) {
return object_freeze(unstate(value));
return object_freeze(snapshot(value));
}
// Otherwise freeze the object
object_freeze(value);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { test } from '../../test';

export default test({
error: {
code: 'invalid-rune-args-length',
message: '$state.snapshot can only be called with 1 argument'
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<script>
const foo = $state.snapshot();
</script>
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
<script>
import { unstate } from 'svelte';

let items = $state([{a: 0}]);
</script>

<button on:click={() => items.push({a: items.length})}>{JSON.stringify(structuredClone(unstate(items)))}</button>
<button on:click={() => items.push({a: items.length})}>{JSON.stringify(structuredClone($state.snapshot(items)))}</button>
21 changes: 20 additions & 1 deletion packages/svelte/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,6 @@ declare module 'svelte' {
export function flushSync(fn?: (() => void) | undefined): void;
/** Anything except a function */
type NotFunction<T> = T extends Function ? never : T;
export function unstate<T>(value: T): T;
/**
* Mounts a component to the given target and returns the exports and potentially the props (if compiled with `accessors: true`) of the component
*
Expand Down Expand Up @@ -2551,6 +2550,26 @@ declare namespace $state {
*/
export function frozen<T>(initial: T): Readonly<T>;
export function frozen<T>(): Readonly<T> | undefined;
/**
* To take a static snapshot of a deeply reactive `$state` proxy, use `$state.snapshot`:
*
* Example:
* ```ts
* <script>
* let counter = $state({ count: 0 });
*
* function onclick() {
* // Will log `{ count: ... }` rather than `Proxy { ... }`
* console.log($state.snapshot(counter));
* };
* </script>
* ```
*
* https://svelte-5-preview.vercel.app/docs/runes#$state.snapshot
*
* @param state The value to snapshot
*/
export function snapshot<T>(state: T): T;
}

/**
Expand Down
19 changes: 10 additions & 9 deletions sites/svelte-5-preview/src/lib/CodeMirror.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -205,27 +205,28 @@
return {
from: word.from - 1,
options: [
{ label: '$state', type: 'keyword', boost: 10 },
{ label: '$props', type: 'keyword', boost: 9 },
{ label: '$derived', type: 'keyword', boost: 8 },
{ label: '$state', type: 'keyword', boost: 12 },
{ label: '$props', type: 'keyword', boost: 11 },
{ label: '$derived', type: 'keyword', boost: 10 },
snip('$derived.by(() => {\n\t${}\n});', {
label: '$derived.by',
type: 'keyword',
boost: 7
boost: 9
}),
snip('$effect(() => {\n\t${}\n});', { label: '$effect', type: 'keyword', boost: 6 }),
snip('$effect(() => {\n\t${}\n});', { label: '$effect', type: 'keyword', boost: 8 }),
snip('$effect.pre(() => {\n\t${}\n});', {
label: '$effect.pre',
type: 'keyword',
boost: 5
boost: 7
}),
{ label: '$state.frozen', type: 'keyword', boost: 4 },
{ label: '$bindable', type: 'keyword', boost: 4 },
{ label: '$state.frozen', type: 'keyword', boost: 6 },
{ label: '$bindable', type: 'keyword', boost: 5 },
snip('$effect.root(() => {\n\t${}\n});', {
label: '$effect.root',
type: 'keyword',
boost: 3
boost: 4
}),
{ label: '$state.snapshot', type: 'keyword', boost: 3 },
snip('$effect.active()', {
label: '$effect.active',
type: 'keyword',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ class Todo {

> In this example, the compiler transforms `done` and `text` into `get`/`set` methods on the class prototype referencing private fields

Objects and arrays [are made deeply reactive](/#H4sIAAAAAAAAE42QwWrDMBBEf2URhUhUNEl7c21DviPOwZY3jVpZEtIqUBz9e-UUt9BTj7M784bdmZ21wciq48xsPyGr2MF7Jhl9-kXEKxrCoqNLQS2TOqqgPbWd7cgggU3TgCFCAw-RekJ-3Et4lvByEq-drbe_dlsPichZcFYZrT6amQto2pXw5FO88FUYtG90gUfYi3zvWrYL75vxL57zfA07_zfr23k1vjtt-aZ0bQTcbrDL5ZifZcAxKeS8lzDc8X0xDhJ2ItdbX1jlOZMb9VnjyCoKCfMpfwG975NFVwEAAA==):
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):

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

## `$state.snapshot`

To take a static snapshot of a deeply reactive `$state` proxy, use `$state.snapshot`:

```svelte
<script>
let counter = $state({ count: 0 });

function onclick() {
// Will log `{ count: ... }` rather than `Proxy { ... }`
console.log($state.snapshot(counter));
}
</script>
```

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`.

> 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.

## `$derived`

Derived state is declared with the `$derived` rune:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,27 +23,6 @@ To prevent something from being treated as an `$effect`/`$derived` dependency, u
</script>
```

## `unstate`

To remove reactivity from objects and arrays created with `$state`, use `unstate`:

```svelte
<script>
import { unstate } from 'svelte';

let counter = $state({ count: 0 });

$effect(() => {
// Will log { count: 0 }
console.log(unstate(counter));
});
</script>
```

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`.

> 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.

## `mount`

Instantiates a component and mounts it to the given target:
Expand Down