Skip to content
Open
5 changes: 5 additions & 0 deletions .changeset/sharp-rings-march.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': minor
---

feat: allow `$state` in return statements
13 changes: 13 additions & 0 deletions documentation/docs/02-runes/02-$state.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,19 @@ let { done, text } = todos[0];
todos[0].done = !todos[0].done;
```

You can also use `$state` in return statements to proxy their argument:

```js
function createCounter() {
return $state({
count: 0,
increment() {
this.count++;
}
});
}
```

### Classes

You can also use `$state` in class fields (whether public or private):
Expand Down
6 changes: 6 additions & 0 deletions documentation/docs/98-reference/.generated/client-warnings.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,12 @@ Reactive `$state(...)` proxies and the values they proxy have different identiti

To resolve this, ensure you're comparing values where both values were created with `$state(...)`, or neither were. Note that `$state.raw(...)` will _not_ create a state proxy.

### state_return_not_proxyable

```
The argument passed to a `$state` call in a return statement must be a plain object or array. Otherwise, the `$state` call will have no effect
```

### transition_slide_display

```
Expand Down
4 changes: 4 additions & 0 deletions packages/svelte/messages/client-warnings/warnings.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,10 @@ To fix it, either create callback props to communicate changes, or mark `person`

To resolve this, ensure you're comparing values where both values were created with `$state(...)`, or neither were. Note that `$state.raw(...)` will _not_ create a state proxy.

## state_return_not_proxyable

> The argument passed to a `$state` call in a return statement must be a plain object or array. Otherwise, the `$state` call will have no effect

## transition_slide_display

> The `slide` transition does not work correctly for elements with `display: %value%`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,20 @@ export function CallExpression(node, context) {
}

case '$state':
if (
(!(parent.type === 'VariableDeclarator' || parent.type === 'ReturnStatement') ||
get_parent(context.path, -3).type === 'ConstTag') &&
!(parent.type === 'PropertyDefinition' && !parent.static && !parent.computed) &&
!(parent.type === 'ArrowFunctionExpression' && parent.body === node)
) {
e.state_invalid_placement(node, rune);
}

if (node.arguments.length > 1) {
e.rune_invalid_arguments_length(node, rune, 'zero or one arguments');
}

break;
case '$state.raw':
case '$derived':
case '$derived.by':
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import { dev, is_ignored } from '../../../../state.js';
import * as b from '#compiler/builders';
import { get_rune } from '../../../scope.js';
import { transform_inspect_rune } from '../../utils.js';
import { should_proxy } from '../utils.js';

/**
* @param {CallExpression} node
* @param {Context} context
*/
export function CallExpression(node, context) {
const parent = context.path.at(-1);
switch (get_rune(node, context.state.scope)) {
case '$host':
return b.id('$$props.$$host');
Expand All @@ -33,6 +35,18 @@ export function CallExpression(node, context) {
case '$inspect':
case '$inspect().with':
return transform_inspect_rune(node, context);
case '$state':
if (
parent?.type === 'ReturnStatement' ||
(parent?.type === 'ArrowFunctionExpression' && parent.body === node)
) {
if (node.arguments[0]) {
return b.call(
'$.return_proxy',
/** @type {Expression} */ (context.visit(node.arguments[0] ?? b.void0))
);
}
}
}

if (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/** @import { CallExpression, Expression } from 'estree' */
/** @import { ArrowFunctionExpression, CallExpression, Expression } from 'estree' */
/** @import { Context } from '../types.js' */
import { is_ignored } from '../../../../state.js';
import * as b from '#compiler/builders';
Expand Down Expand Up @@ -37,5 +37,17 @@ export function CallExpression(node, context) {
return transform_inspect_rune(node, context);
}

if (
rune === '$state' &&
(context.path.at(-1)?.type === 'ReturnStatement' ||
(context.path.at(-1)?.type === 'ArrowFunctionExpression' &&
/** @type {ArrowFunctionExpression} */ (context.path.at(-1)).body === node))
) {
if (node.arguments[0]) {
return context.visit(node.arguments[0]);
}
return b.void0;
}

context.next();
}
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 @@ -141,7 +141,7 @@ export {
} from './runtime.js';
export { validate_binding, validate_each_keys } from './validate.js';
export { raf } from './timing.js';
export { proxy } from './proxy.js';
export { proxy, return_proxy } from './proxy.js';
export { create_custom_element } from './dom/elements/custom-element.js';
export {
child,
Expand Down
39 changes: 32 additions & 7 deletions packages/svelte/src/internal/client/proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,33 @@ import { state as source, set } from './reactivity/sources.js';
import { STATE_SYMBOL } from '#client/constants';
import { UNINITIALIZED } from '../../constants.js';
import * as e from './errors.js';
import * as w from './warnings.js';
import { get_stack } from './dev/tracing.js';
import { tracing_mode_flag } from '../flags/index.js';

/**
* @param {unknown} value
* @returns {boolean}
*/
function should_proxy(value) {
if (typeof value !== 'object' || value === null || STATE_SYMBOL in value) {
return false;
}
const prototype = get_prototype_of(value);
if (prototype !== object_prototype && prototype !== array_prototype) {
return false;
}
return true;
}

/**
* @template T
* @param {T} value
* @returns {T}
*/
export function proxy(value) {
// if non-proxyable, or is already a proxy, return `value`
if (typeof value !== 'object' || value === null || STATE_SYMBOL in value) {
return value;
}

const prototype = get_prototype_of(value);

if (prototype !== object_prototype && prototype !== array_prototype) {
if (!should_proxy(value)) {
return value;
}

Expand Down Expand Up @@ -282,6 +292,21 @@ export function proxy(value) {
});
}

/**
* @template T
* @param {T} value
* @returns {T | void}
*/
export function return_proxy(value) {
if (should_proxy(value)) {
return proxy(value);
} else if (DEV && !(typeof value === 'object' && value !== null && STATE_SYMBOL in value)) {
// if the argument passed was already a proxy, we don't warn
w.state_return_not_proxyable();
}
return value;
}

/**
* @param {Source<number>} signal
* @param {1 | -1} [d]
Expand Down
11 changes: 11 additions & 0 deletions packages/svelte/src/internal/client/warnings.js
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,17 @@ export function state_proxy_equality_mismatch(operator) {
}
}

/**
* The argument passed to a `$state` call in a return statement must be a plain object or array. Otherwise, the `$state` call will have no effect
*/
export function state_return_not_proxyable() {
if (DEV) {
console.warn(`%c[svelte] state_return_not_proxyable\n%cThe argument passed to a \`$state\` call in a return statement must be a plain object or array. Otherwise, the \`$state\` call will have no effect\nhttps://svelte.dev/e/state_return_not_proxyable`, bold, normal);
} else {
console.warn(`https://svelte.dev/e/state_return_not_proxyable`);
}
}

/**
* The `slide` transition does not work correctly for elements with `display: %value%`
* @param {string} value
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/* index.svelte.js generated by Svelte VERSION */
import * as $ from 'svelte/internal/client';

export default function proxy(object) {
return $.return_proxy(object);
}

export function createCounter() {
let count = $.state(0);

$.update(count);
}

export const proxy_in_arrow = (object) => $.return_proxy(object);
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/* index.svelte.js generated by Svelte VERSION */
import * as $ from 'svelte/internal/server';

export default function proxy(object) {
return object;
}

export function createCounter() {
let count = 0;

count++;
}

export const proxy_in_arrow = (object) => object;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export default function proxy(object) {
return $state(object);
}
export function createCounter() {
let count = $state(0);
count++;
}
export const proxy_in_arrow = (object) => $state(object);