Skip to content

Commit f20cfec

Browse files
committed
feat: add $state.is rune
1 parent a8a5bb6 commit f20cfec

File tree

15 files changed

+138
-5
lines changed

15 files changed

+138
-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: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,28 @@ declare namespace $state {
6363
*/
6464
export function snapshot<T>(state: T): T;
6565

66+
/**
67+
* To take a check if two reactive from `$state()` are the same, use `$state.is`:
68+
*
69+
* Example:
70+
* ```ts
71+
* <script>
72+
* let state = $state({});
73+
* let object = {};
74+
*
75+
* state.object = object;
76+
*
77+
* console.log(state.object === object); // false because of the $state proxy
78+
*
79+
* console.log($state.is(state.object, object)); // true
80+
* </script>
81+
* ```
82+
*
83+
* https://svelte-5-preview.vercel.app/docs/runes#$state.is
84+
*
85+
*/
86+
export function is(a: unknown, b: unknown): boolean;
87+
6688
// prevent intellisense from being unhelpful
6789
/** @deprecated */
6890
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
@@ -778,6 +778,13 @@ const javascript_visitors_runes = {
778778
return /** @type {import('estree').Expression} */ (context.visit(node.arguments[0]));
779779
}
780780

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

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+
let state = $state({});
3+
let object = {};
4+
5+
state.object = object;
6+
7+
console.log(state.object === object); // false because of the $state proxy
8+
9+
console.log($state.is(state.object, object)); // true
10+
</script>

packages/svelte/types/index.d.ts

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

2627+
/**
2628+
* To take a check if two reactive from `$state()` are the same, use `$state.is`:
2629+
*
2630+
* Example:
2631+
* ```ts
2632+
* <script>
2633+
* let state = $state({});
2634+
* let object = {};
2635+
*
2636+
* state.object = object;
2637+
*
2638+
* console.log(state.object === object); // false because of the $state proxy
2639+
*
2640+
* console.log($state.is(state.object, object)); // true
2641+
* </script>
2642+
* ```
2643+
*
2644+
* https://svelte-5-preview.vercel.app/docs/runes#$state.is
2645+
*
2646+
*/
2647+
export function is(a: unknown, b: unknown): boolean;
2648+
26272649
// prevent intellisense from being unhelpful
26282650
/** @deprecated */
26292651
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: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,26 @@ 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 deal with the equality of two values, where one of the objects might have been wrapped in a with a `$state` proxy,
118+
you can use `$state.is` to check if the two values are the same.
119+
120+
```svelte
121+
<script>
122+
let state = $state({});
123+
let object = {};
124+
125+
state.object = object;
126+
127+
console.log(state.object === object); // false because of the $state proxy
128+
129+
console.log($state.is(state.object, object)); // true
130+
</script>
131+
```
132+
133+
This is handy when you might want to check if the object exists within a deeply reactive object/array.
134+
115135
## `$derived`
116136

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

0 commit comments

Comments
 (0)