Skip to content

Commit c7e626e

Browse files
trueadmdummdidummRich-Harris
authored
feat: add unstate utility function (#9776)
* feat: add unstate utility function * Update packages/svelte/src/internal/client/proxy/proxy.js Co-authored-by: Simon H <[email protected]> * update docs * add class support * oops * lint * fix docs * remove symbol and class support --------- Co-authored-by: Simon H <[email protected]> Co-authored-by: Rich Harris <[email protected]>
1 parent f1954d0 commit c7e626e

File tree

15 files changed

+140
-15
lines changed

15 files changed

+140
-15
lines changed

.changeset/beige-flies-wash.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+
feat: add unstate utility function

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,12 @@ export const javascript_visitors_runes = {
105105
// set foo(value) { this.#foo = value; }
106106
const value = b.id('value');
107107
body.push(
108-
b.method('set', definition.key, [value], [b.stmt(b.call('$.set', member, value))])
108+
b.method(
109+
'set',
110+
definition.key,
111+
[value],
112+
[b.stmt(b.call('$.set', member, b.call('$.proxy', value)))]
113+
)
109114
);
110115
}
111116

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

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,13 @@ import {
88
updating_derived,
99
UNINITIALIZED
1010
} from '../runtime.js';
11-
import { define_property, get_descriptor, is_array } from '../utils.js';
11+
import {
12+
define_property,
13+
get_descriptor,
14+
get_descriptors,
15+
is_array,
16+
object_keys
17+
} from '../utils.js';
1218
import { READONLY_SYMBOL } from './readonly.js';
1319

1420
/** @typedef {{ s: Map<string | symbol, import('../types.js').SourceSignal<any>>; v: import('../types.js').SourceSignal<number>; a: boolean }} Metadata */
@@ -42,6 +48,56 @@ export function proxy(value) {
4248
return value;
4349
}
4450

51+
/**
52+
* @template {StateObject} T
53+
* @param {T} value
54+
* @param {Map<T, Record<string | symbol, any>>} already_unwrapped
55+
* @returns {Record<string | symbol, any>}
56+
*/
57+
function unwrap(value, already_unwrapped = new Map()) {
58+
if (typeof value === 'object' && value != null && !is_frozen(value) && STATE_SYMBOL in value) {
59+
const unwrapped = already_unwrapped.get(value);
60+
if (unwrapped !== undefined) {
61+
return unwrapped;
62+
}
63+
if (is_array(value)) {
64+
/** @type {Record<string | symbol, any>} */
65+
const array = [];
66+
already_unwrapped.set(value, array);
67+
for (const element of value) {
68+
array.push(unwrap(element, already_unwrapped));
69+
}
70+
return array;
71+
} else {
72+
/** @type {Record<string | symbol, any>} */
73+
const obj = {};
74+
const keys = object_keys(value);
75+
const descriptors = get_descriptors(value);
76+
already_unwrapped.set(value, obj);
77+
for (const key of keys) {
78+
if (descriptors[key].get) {
79+
define_property(obj, key, descriptors[key]);
80+
} else {
81+
/** @type {T} */
82+
const property = value[key];
83+
obj[key] = unwrap(property, already_unwrapped);
84+
}
85+
}
86+
return obj;
87+
}
88+
}
89+
return value;
90+
}
91+
92+
/**
93+
* @template {StateObject} T
94+
* @param {T} value
95+
* @returns {Record<string | symbol, any>}
96+
*/
97+
export function unstate(value) {
98+
return unwrap(value);
99+
}
100+
45101
/**
46102
* @param {StateObject} value
47103
* @returns {Metadata}

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

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,16 @@ import {
5050
hydrate_block_anchor,
5151
set_current_hydration_fragment
5252
} from './hydration.js';
53-
import { array_from, define_property, get_descriptor, get_descriptors, is_array } from './utils.js';
53+
import {
54+
array_from,
55+
define_property,
56+
get_descriptor,
57+
get_descriptors,
58+
is_array,
59+
object_assign,
60+
object_entries,
61+
object_keys
62+
} from './utils.js';
5463
import { is_promise } from '../common.js';
5564
import { bind_transition, trigger_transitions } from './transitions.js';
5665

@@ -2402,7 +2411,7 @@ function get_setters(element) {
24022411
* @returns {Record<string, unknown>}
24032412
*/
24042413
export function spread_attributes(dom, prev, attrs, lowercase_attributes, css_hash) {
2405-
const next = Object.assign({}, ...attrs);
2414+
const next = object_assign({}, ...attrs);
24062415
const has_hash = css_hash.length !== 0;
24072416
for (const key in prev) {
24082417
if (!(key in next)) {
@@ -2498,7 +2507,7 @@ export function spread_attributes(dom, prev, attrs, lowercase_attributes, css_ha
24982507
*/
24992508
export function spread_dynamic_element_attributes(node, prev, attrs, css_hash) {
25002509
if (node.tagName.includes('-')) {
2501-
const next = Object.assign({}, ...attrs);
2510+
const next = object_assign({}, ...attrs);
25022511
if (prev !== null) {
25032512
for (const key in prev) {
25042513
if (!(key in next)) {
@@ -2666,7 +2675,7 @@ export function createRoot(component, options) {
26662675
const result =
26672676
/** @type {Exports & { $destroy: () => void; $set: (props: Partial<Props>) => void; }} */ ({
26682677
$set: (props) => {
2669-
for (const [prop, value] of Object.entries(props)) {
2678+
for (const [prop, value] of object_entries(props)) {
26702679
if (prop in _sources) {
26712680
set(_sources[prop], value);
26722681
} else {
@@ -2678,7 +2687,7 @@ export function createRoot(component, options) {
26782687
$destroy
26792688
});
26802689

2681-
for (const key of Object.keys(accessors || {})) {
2690+
for (const key of object_keys(accessors || {})) {
26822691
define_property(result, key, {
26832692
get() {
26842693
// @ts-expect-error TS doesn't know key exists on accessor

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
// to de-opt (this occurs often when using popular extensions).
33
export var is_array = Array.isArray;
44
export var array_from = Array.from;
5+
export var object_keys = Object.keys;
6+
export var object_entries = Object.entries;
7+
export var object_assign = Object.assign;
58
export var define_property = Object.defineProperty;
69
export var get_descriptor = Object.getOwnPropertyDescriptor;
710
export var get_descriptors = Object.getOwnPropertyDescriptors;

packages/svelte/src/internal/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export * from './client/each.js';
4545
export * from './client/render.js';
4646
export * from './client/validate.js';
4747
export { raf } from './client/timing.js';
48-
export { proxy, readonly } from './client/proxy/proxy.js';
48+
export { proxy, readonly, unstate } from './client/proxy/proxy.js';
4949

5050
export { create_custom_element } from './client/custom-element.js';
5151

packages/svelte/src/main/main-client.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,4 +255,12 @@ export function afterUpdate(fn) {
255255

256256
// TODO bring implementations in here
257257
// (except probably untrack — do we want to expose that, if there's also a rune?)
258-
export { flushSync, createRoot, mount, tick, untrack, onDestroy } from '../internal/index.js';
258+
export {
259+
flushSync,
260+
createRoot,
261+
mount,
262+
tick,
263+
untrack,
264+
unstate,
265+
onDestroy
266+
} from '../internal/index.js';
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
html: `<button>[{"a":0}]</button>`,
5+
6+
async test({ assert, target }) {
7+
const btn = target.querySelector('button');
8+
9+
await btn?.click();
10+
assert.htmlEqual(target.innerHTML, `<button>[{"a":0},{"a":1}]</button>`);
11+
}
12+
});
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<script>
2+
import { unstate } from 'svelte';
3+
4+
let items = $state([{a: 0}]);
5+
</script>
6+
7+
<button on:click={() => items.push({a: items.length})}>{JSON.stringify(structuredClone(unstate(items)))}</button>

packages/svelte/tests/snapshot/samples/class-state-field-constructor-assignment/_expected/client/index.svelte.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export default function Class_state_field_constructor_assignment($$anchor, $$pro
1414
}
1515

1616
set a(value) {
17-
$.set(this.#a, value);
17+
$.set(this.#a, $.proxy(value));
1818
}
1919

2020
#b = $.source();

packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/client/main.svelte.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,4 @@ export default function Main($$anchor, $$props) {
4545

4646
$.close_frag($$anchor, fragment);
4747
$.pop();
48-
}
48+
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
/* index.svelte.js generated by Svelte VERSION */
22
import * as $ from "svelte/internal";
33

4-
export const object = $.proxy({ ok: true });
4+
export const object = $.proxy({ ok: true });
Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
/* index.svelte.js generated by Svelte VERSION */
22
import * as $ from "svelte/internal/server";
33

4-
export const object = { ok: true };
5-
4+
export const object = { ok: true };

packages/svelte/tests/snapshot/samples/svelte-element/_expected/client/index.svelte.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,4 @@ export default function Svelte_element($$anchor, $$props) {
1414
$.element(node, () => $.get(tag));
1515
$.close_frag($$anchor, fragment);
1616
$.pop();
17-
}
17+
}

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,24 @@ To prevent something from being treated as an `$effect`/`$derived` dependency, u
2222
});
2323
</script>
2424
```
25+
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.

0 commit comments

Comments
 (0)