Skip to content

feat: allow $state in return statements #15589

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

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
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);