Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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.clean rune
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,14 @@ export const javascript_visitors_runes = {
return b.call('$.effect_active');
}

if (rune === '$state.clean') {
const arg =
node.arguments.length > 0
? /** @type {import('estree').Expression} */ (context.visit(node.arguments[0]))
: b.id('undefined');
return b.call('$.clean', arg);
}

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,14 @@ const javascript_visitors_runes = {
return b.literal(false);
}

if (rune === '$state.clean') {
const arg =
node.arguments.length > 0
? /** @type {import('estree').Expression} */ (context.visit(node.arguments[0]))
: b.id('undefined');
return context.visit(arg);
}

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.clean',
'$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, clean } 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 clean(value) {
return /** @type {T} */ (
unwrap(/** @type {import('#client').ProxyStateObject} */ (value), new Map())
);
Expand Down
6 changes: 3 additions & 3 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 { clean } from './proxy.js';
import { destroy_effect, effect, user_pre_effect } from './reactivity/effects.js';
import {
EFFECT,
Expand Down Expand Up @@ -1173,7 +1173,7 @@ export function deep_read(value, visited = new Set()) {
*/
function deep_unstate(value, visited = new Map()) {
if (typeof value === 'object' && value !== null && !visited.has(value)) {
const unstated = unstate(value);
const unstated = clean(value);
if (unstated !== value) {
visited.set(value, unstated);
return unstated;
Expand Down Expand Up @@ -1303,7 +1303,7 @@ export function freeze(value) {
if (typeof value === 'object' && value != null && !is_frozen(value)) {
// If the object is already proxified, then unstate the value
if (STATE_SYMBOL in value) {
return object_freeze(unstate(value));
return object_freeze(clean(value));
}
// Otherwise freeze the object
object_freeze(value);
Expand Down
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.clean(items)))}</button>
1 change: 0 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
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.clean', type: 'keyword', boost: 3 },
snip('$effect.active()', {
label: '$effect.active',
type: 'keyword',
Expand Down
19 changes: 19 additions & 0 deletions sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,25 @@ Svelte provides reactive `Map`, `Set` and `Date` classes. These can be imported
<p>{map.get('message')}</p>
```

## `$state.clean`

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

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

$effect(() => {
// Will log { count: 0 }
console.log($state.clean(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`.
Comment thread
Conduitry marked this conversation as resolved.
Outdated

> Note that `$state.clean` will return a new object from the input when removing reactivity. If the object passed isn't reactive, 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