feat: add $state.is rune (#11613)

* feat: add $state.is rune

* fix type

* tweak docs

* may as well update the test case to match the docs

---------

Co-authored-by: Rich Harris <rich.harris@vercel.com>
pull/11621/head
Dominic Gannaway 8 months ago committed by GitHub
parent 1087e6fb54
commit f488a6e84a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
"svelte": patch
---
feat: add $state.is rune

@ -63,6 +63,27 @@ declare namespace $state {
*/ */
export function snapshot<T>(state: T): T; export function snapshot<T>(state: T): T;
/**
* Compare two values, one or both of which is a reactive `$state(...)` proxy.
*
* Example:
* ```ts
* <script>
* let foo = $state({});
* let bar = {};
*
* foo.bar = bar;
*
* console.log(foo.bar === bar); // false — `foo.bar` is a reactive proxy
* console.log($state.is(foo.bar, bar)); // true
* </script>
* ```
*
* https://svelte-5-preview.vercel.app/docs/runes#$state.is
*
*/
export function is(a: any, b: any): boolean;
// prevent intellisense from being unhelpful // prevent intellisense from being unhelpful
/** @deprecated */ /** @deprecated */
export const apply: never; export const apply: never;

@ -865,6 +865,12 @@ function validate_call_expression(node, scope, path) {
e.rune_invalid_arguments_length(node, rune, 'exactly one argument'); e.rune_invalid_arguments_length(node, rune, 'exactly one argument');
} }
} }
if (rune === '$state.is') {
if (node.arguments.length !== 2) {
e.rune_invalid_arguments_length(node, rune, 'exactly two arguments');
}
}
} }
/** /**

@ -209,7 +209,8 @@ export const javascript_visitors_runes = {
rune === '$effect.active' || rune === '$effect.active' ||
rune === '$effect.root' || rune === '$effect.root' ||
rune === '$inspect' || rune === '$inspect' ||
rune === '$state.snapshot' rune === '$state.snapshot' ||
rune === '$state.is'
) { ) {
if (init != null && is_hoistable_function(init)) { if (init != null && is_hoistable_function(init)) {
const hoistable_function = visit(init); const hoistable_function = visit(init);
@ -430,6 +431,14 @@ export const javascript_visitors_runes = {
); );
} }
if (rune === '$state.is') {
return b.call(
'$.is',
/** @type {import('estree').Expression} */ (context.visit(node.arguments[0])),
/** @type {import('estree').Expression} */ (context.visit(node.arguments[1]))
);
}
if (rune === '$effect.root') { if (rune === '$effect.root') {
const args = /** @type {import('estree').Expression[]} */ ( const args = /** @type {import('estree').Expression[]} */ (
node.arguments.map((arg) => context.visit(arg)) node.arguments.map((arg) => context.visit(arg))

@ -779,6 +779,13 @@ const javascript_visitors_runes = {
return /** @type {import('estree').Expression} */ (context.visit(node.arguments[0])); return /** @type {import('estree').Expression} */ (context.visit(node.arguments[0]));
} }
if (rune === '$state.is') {
return b.call(
'Object.is',
/** @type {import('estree').Expression} */ (context.visit(node.arguments[0]))
);
}
if (rune === '$inspect' || rune === '$inspect().with') { if (rune === '$inspect' || rune === '$inspect().with') {
return transform_inspect_rune(node, context); return transform_inspect_rune(node, context);
} }

@ -32,6 +32,7 @@ export const Runes = /** @type {const} */ ([
'$state', '$state',
'$state.frozen', '$state.frozen',
'$state.snapshot', '$state.snapshot',
'$state.is',
'$props', '$props',
'$bindable', '$bindable',
'$derived', '$derived',

@ -3,6 +3,7 @@ import { render_effect, effect } from '../../../reactivity/effects.js';
import { stringify } from '../../../render.js'; import { stringify } from '../../../render.js';
import { listen_to_event_and_reset_event } from './shared.js'; import { listen_to_event_and_reset_event } from './shared.js';
import * as e from '../../../errors.js'; import * as e from '../../../errors.js';
import { get_proxied_value, is } from '../../../proxy.js';
/** /**
* @param {HTMLInputElement} input * @param {HTMLInputElement} input
@ -95,10 +96,10 @@ export function bind_group(inputs, group_index, input, get_value, update) {
if (is_checkbox) { if (is_checkbox) {
value = value || []; value = value || [];
// @ts-ignore // @ts-ignore
input.checked = value.includes(input.__value); input.checked = get_proxied_value(value).includes(get_proxied_value(input.__value));
} else { } else {
// @ts-ignore // @ts-ignore
input.checked = input.__value === value; input.checked = is(input.__value, value);
} }
}); });

@ -1,6 +1,7 @@
import { effect } from '../../../reactivity/effects.js'; import { effect } from '../../../reactivity/effects.js';
import { listen_to_event_and_reset_event } from './shared.js'; import { listen_to_event_and_reset_event } from './shared.js';
import { untrack } from '../../../runtime.js'; import { untrack } from '../../../runtime.js';
import { is } from '../../../proxy.js';
/** /**
* Selects the correct option(s) (depending on whether this is a multiple select) * Selects the correct option(s) (depending on whether this is a multiple select)
@ -16,7 +17,7 @@ export function select_option(select, value, mounting) {
for (var option of select.options) { for (var option of select.options) {
var option_value = get_option_value(option); var option_value = get_option_value(option);
if (option_value === value) { if (is(option_value, value)) {
option.selected = true; option.selected = true;
return; return;
} }

@ -143,7 +143,7 @@ export {
validate_prop_bindings validate_prop_bindings
} from './validate.js'; } from './validate.js';
export { raf } from './timing.js'; export { raf } from './timing.js';
export { proxy, snapshot } from './proxy.js'; export { proxy, snapshot, is } from './proxy.js';
export { create_custom_element } from './dom/elements/custom-element.js'; export { create_custom_element } from './dom/elements/custom-element.js';
export { export {
child, child,

@ -337,3 +337,24 @@ if (DEV) {
e.state_prototype_fixed(); e.state_prototype_fixed();
}; };
} }
/**
* @param {any} value
*/
export function get_proxied_value(value) {
if (value !== null && typeof value === 'object' && STATE_SYMBOL in value) {
var metadata = value[STATE_SYMBOL];
if (metadata) {
return metadata.p;
}
}
return value;
}
/**
* @param {any} a
* @param {any} b
*/
export function is(a, b) {
return Object.is(get_proxied_value(a), get_proxied_value(b));
}

@ -0,0 +1,7 @@
import { test } from '../../test';
export default test({
async test({ assert, logs }) {
assert.deepEqual(logs, [false, true]);
}
});

@ -0,0 +1,10 @@
<script>
/** @type {{ bar?: any }}*/
let foo = $state({});
let bar = {};
foo.bar = bar;
console.log(foo.bar === bar); // false because of the $state proxy
console.log($state.is(foo.bar, bar)); // true
</script>

@ -2624,6 +2624,27 @@ declare namespace $state {
*/ */
export function snapshot<T>(state: T): T; export function snapshot<T>(state: T): T;
/**
* Compare two values, one or both of which is a reactive `$state(...)` proxy.
*
* Example:
* ```ts
* <script>
* let foo = $state({});
* let bar = {};
*
* foo.bar = bar;
*
* console.log(foo.bar === bar); // false — `foo.bar` is a reactive proxy
* console.log($state.is(foo.bar, bar)); // true
* </script>
* ```
*
* https://svelte-5-preview.vercel.app/docs/runes#$state.is
*
*/
export function is(a: any, b: any): boolean;
// prevent intellisense from being unhelpful // prevent intellisense from being unhelpful
/** @deprecated */ /** @deprecated */
export const apply: never; export const apply: never;

@ -118,6 +118,7 @@ const runes = [
{ snippet: '$bindable()', test: is_bindable }, { snippet: '$bindable()', test: is_bindable },
{ snippet: '$effect.root(() => {\n\t${}\n})' }, { snippet: '$effect.root(() => {\n\t${}\n})' },
{ snippet: '$state.snapshot(${})' }, { snippet: '$state.snapshot(${})' },
{ snippet: '$state.is(${})' },
{ snippet: '$effect.active()' }, { snippet: '$effect.active()' },
{ snippet: '$inspect(${});', test: is_statement } { snippet: '$inspect(${});', test: is_statement }
]; ];

@ -112,6 +112,24 @@ This is handy when you want to pass some state to an external library or API tha
> 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. > 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.
## `$state.is`
Sometimes you might need to compare two values, one of which is a reactive `$state(...)` proxy. For this you can use `$state.is(a, b)`:
```svelte
<script>
let foo = $state({});
let bar = {};
foo.bar = bar;
console.log(foo.bar === bar); // false — `foo.bar` is a reactive proxy
console.log($state.is(foo.bar, bar)); // true
</script>
```
This is handy when you might want to check if the object exists within a deeply reactive object/array.
## `$derived` ## `$derived`
Derived state is declared with the `$derived` rune: Derived state is declared with the `$derived` rune:

Loading…
Cancel
Save