Skip to content

Commit f488a6e

Browse files
trueadmRich-Harris
andauthored
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 <[email protected]>
1 parent 1087e6f commit f488a6e

File tree

15 files changed

+134
-5
lines changed

15 files changed

+134
-5
lines changed

.changeset/khaki-monkeys-cry.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 $state.is rune

packages/svelte/src/ambient.d.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,27 @@ declare namespace $state {
6363
*/
6464
export function snapshot<T>(state: T): T;
6565

66+
/**
67+
* Compare two values, one or both of which is a reactive `$state(...)` proxy.
68+
*
69+
* Example:
70+
* ```ts
71+
* <script>
72+
* let foo = $state({});
73+
* let bar = {};
74+
*
75+
* foo.bar = bar;
76+
*
77+
* console.log(foo.bar === bar); // false — `foo.bar` is a reactive proxy
78+
* console.log($state.is(foo.bar, bar)); // true
79+
* </script>
80+
* ```
81+
*
82+
* https://svelte-5-preview.vercel.app/docs/runes#$state.is
83+
*
84+
*/
85+
export function is(a: any, b: any): boolean;
86+
6687
// prevent intellisense from being unhelpful
6788
/** @deprecated */
6889
export const apply: never;

packages/svelte/src/compiler/phases/2-analyze/validation.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -865,6 +865,12 @@ function validate_call_expression(node, scope, path) {
865865
e.rune_invalid_arguments_length(node, rune, 'exactly one argument');
866866
}
867867
}
868+
869+
if (rune === '$state.is') {
870+
if (node.arguments.length !== 2) {
871+
e.rune_invalid_arguments_length(node, rune, 'exactly two arguments');
872+
}
873+
}
868874
}
869875

870876
/**

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,8 @@ export const javascript_visitors_runes = {
209209
rune === '$effect.active' ||
210210
rune === '$effect.root' ||
211211
rune === '$inspect' ||
212-
rune === '$state.snapshot'
212+
rune === '$state.snapshot' ||
213+
rune === '$state.is'
213214
) {
214215
if (init != null && is_hoistable_function(init)) {
215216
const hoistable_function = visit(init);
@@ -430,6 +431,14 @@ export const javascript_visitors_runes = {
430431
);
431432
}
432433

434+
if (rune === '$state.is') {
435+
return b.call(
436+
'$.is',
437+
/** @type {import('estree').Expression} */ (context.visit(node.arguments[0])),
438+
/** @type {import('estree').Expression} */ (context.visit(node.arguments[1]))
439+
);
440+
}
441+
433442
if (rune === '$effect.root') {
434443
const args = /** @type {import('estree').Expression[]} */ (
435444
node.arguments.map((arg) => context.visit(arg))

packages/svelte/src/compiler/phases/3-transform/server/transform-server.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -779,6 +779,13 @@ const javascript_visitors_runes = {
779779
return /** @type {import('estree').Expression} */ (context.visit(node.arguments[0]));
780780
}
781781

782+
if (rune === '$state.is') {
783+
return b.call(
784+
'Object.is',
785+
/** @type {import('estree').Expression} */ (context.visit(node.arguments[0]))
786+
);
787+
}
788+
782789
if (rune === '$inspect' || rune === '$inspect().with') {
783790
return transform_inspect_rune(node, context);
784791
}

packages/svelte/src/compiler/phases/constants.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export const Runes = /** @type {const} */ ([
3232
'$state',
3333
'$state.frozen',
3434
'$state.snapshot',
35+
'$state.is',
3536
'$props',
3637
'$bindable',
3738
'$derived',

packages/svelte/src/internal/client/dom/elements/bindings/input.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { render_effect, effect } from '../../../reactivity/effects.js';
33
import { stringify } from '../../../render.js';
44
import { listen_to_event_and_reset_event } from './shared.js';
55
import * as e from '../../../errors.js';
6+
import { get_proxied_value, is } from '../../../proxy.js';
67

78
/**
89
* @param {HTMLInputElement} input
@@ -95,10 +96,10 @@ export function bind_group(inputs, group_index, input, get_value, update) {
9596
if (is_checkbox) {
9697
value = value || [];
9798
// @ts-ignore
98-
input.checked = value.includes(input.__value);
99+
input.checked = get_proxied_value(value).includes(get_proxied_value(input.__value));
99100
} else {
100101
// @ts-ignore
101-
input.checked = input.__value === value;
102+
input.checked = is(input.__value, value);
102103
}
103104
});
104105

packages/svelte/src/internal/client/dom/elements/bindings/select.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { effect } from '../../../reactivity/effects.js';
22
import { listen_to_event_and_reset_event } from './shared.js';
33
import { untrack } from '../../../runtime.js';
4+
import { is } from '../../../proxy.js';
45

56
/**
67
* Selects the correct option(s) (depending on whether this is a multiple select)
@@ -16,7 +17,7 @@ export function select_option(select, value, mounting) {
1617

1718
for (var option of select.options) {
1819
var option_value = get_option_value(option);
19-
if (option_value === value) {
20+
if (is(option_value, value)) {
2021
option.selected = true;
2122
return;
2223
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ export {
143143
validate_prop_bindings
144144
} from './validate.js';
145145
export { raf } from './timing.js';
146-
export { proxy, snapshot } from './proxy.js';
146+
export { proxy, snapshot, is } from './proxy.js';
147147
export { create_custom_element } from './dom/elements/custom-element.js';
148148
export {
149149
child,

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,3 +337,24 @@ if (DEV) {
337337
e.state_prototype_fixed();
338338
};
339339
}
340+
341+
/**
342+
* @param {any} value
343+
*/
344+
export function get_proxied_value(value) {
345+
if (value !== null && typeof value === 'object' && STATE_SYMBOL in value) {
346+
var metadata = value[STATE_SYMBOL];
347+
if (metadata) {
348+
return metadata.p;
349+
}
350+
}
351+
return value;
352+
}
353+
354+
/**
355+
* @param {any} a
356+
* @param {any} b
357+
*/
358+
export function is(a, b) {
359+
return Object.is(get_proxied_value(a), get_proxied_value(b));
360+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
async test({ assert, logs }) {
5+
assert.deepEqual(logs, [false, true]);
6+
}
7+
});
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<script>
2+
/** @type {{ bar?: any }}*/
3+
let foo = $state({});
4+
let bar = {};
5+
6+
foo.bar = bar;
7+
8+
console.log(foo.bar === bar); // false because of the $state proxy
9+
console.log($state.is(foo.bar, bar)); // true
10+
</script>

packages/svelte/types/index.d.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2624,6 +2624,27 @@ declare namespace $state {
26242624
*/
26252625
export function snapshot<T>(state: T): T;
26262626

2627+
/**
2628+
* Compare two values, one or both of which is a reactive `$state(...)` proxy.
2629+
*
2630+
* Example:
2631+
* ```ts
2632+
* <script>
2633+
* let foo = $state({});
2634+
* let bar = {};
2635+
*
2636+
* foo.bar = bar;
2637+
*
2638+
* console.log(foo.bar === bar); // false — `foo.bar` is a reactive proxy
2639+
* console.log($state.is(foo.bar, bar)); // true
2640+
* </script>
2641+
* ```
2642+
*
2643+
* https://svelte-5-preview.vercel.app/docs/runes#$state.is
2644+
*
2645+
*/
2646+
export function is(a: any, b: any): boolean;
2647+
26272648
// prevent intellisense from being unhelpful
26282649
/** @deprecated */
26292650
export const apply: never;

sites/svelte-5-preview/src/lib/autocomplete.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ const runes = [
118118
{ snippet: '$bindable()', test: is_bindable },
119119
{ snippet: '$effect.root(() => {\n\t${}\n})' },
120120
{ snippet: '$state.snapshot(${})' },
121+
{ snippet: '$state.is(${})' },
121122
{ snippet: '$effect.active()' },
122123
{ snippet: '$inspect(${});', test: is_statement }
123124
];

sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,24 @@ This is handy when you want to pass some state to an external library or API tha
112112

113113
> 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.
114114
115+
## `$state.is`
116+
117+
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)`:
118+
119+
```svelte
120+
<script>
121+
let foo = $state({});
122+
let bar = {};
123+
124+
foo.bar = bar;
125+
126+
console.log(foo.bar === bar); // false — `foo.bar` is a reactive proxy
127+
console.log($state.is(foo.bar, bar)); // true
128+
</script>
129+
```
130+
131+
This is handy when you might want to check if the object exists within a deeply reactive object/array.
132+
115133
## `$derived`
116134

117135
Derived state is declared with the `$derived` rune:

0 commit comments

Comments
 (0)