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 <rich.harris@vercel.com>
pull/11186/head
Dominic Gannaway 1 year ago committed by GitHub
parent cd7c3fea16
commit 18097478fc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
"svelte": patch
---
breaking: remove unstate(), replace with $state.snapshot rune

@ -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;
}
/**

@ -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]);
}
}
}
/**

@ -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))

@ -794,6 +794,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);
}

@ -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',

@ -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 {

@ -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';

@ -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';
/**

@ -128,7 +128,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,

@ -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())
);

@ -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,
@ -1176,26 +1176,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;
@ -1223,7 +1223,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(
@ -1311,9 +1311,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);

@ -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'
}
});

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

@ -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
*
@ -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;
}
/**

@ -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',

@ -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>
@ -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:

@ -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:

Loading…
Cancel
Save