feat: add an onchange option to $state and $state.raw

state-onchange-verified-links
Nic 4 days ago
parent 9a78da982b
commit 0f09d5a4f7

@ -150,6 +150,20 @@ This can improve performance with large arrays and objects that you weren't plan
As with `$state`, you can declare class fields using `$state.raw`.
## State options
`$state` and `$state.raw` accept an options object as their second argument. Its `onchange` function is called synchronously whenever the value is reassigned or, for `$state`, when any of its deeply reactive contents are mutated, before any effects run. Use it to persist state as it changes, or to constrain it:
```js
let count = $state(0, {
onchange() {
count = Math.min(count, 10);
}
});
```
Reads inside `onchange` are not tracked, and a mutation it makes to its own state does not call it again.
## `$state.snapshot`
To take a static snapshot of a deeply reactive `$state` proxy, use `$state.snapshot`:

@ -20,6 +20,11 @@ declare module '*.svelte' {
*
* @param initial The initial value
*/
declare function $state<T>(
initial: undefined,
options?: import('svelte').StateOptions
): T | undefined;
declare function $state<T>(initial: T, options?: import('svelte').StateOptions): T;
declare function $state<T>(initial: T): T;
declare function $state<T>(): T | undefined;
@ -130,6 +135,11 @@ declare namespace $state {
*
* @param initial The initial value
*/
export function raw<T>(
initial: undefined,
options?: import('svelte').StateOptions
): T | undefined;
export function raw<T>(initial?: T, options?: import('svelte').StateOptions): T;
export function raw<T>(initial: T): T;
export function raw<T>(): T | undefined;
/**

@ -127,8 +127,8 @@ export function CallExpression(node, context) {
if ((rune === '$derived' || rune === '$derived.by') && node.arguments.length !== 1) {
e.rune_invalid_arguments_length(node, rune, 'exactly one argument');
} else if (node.arguments.length > 1) {
e.rune_invalid_arguments_length(node, rune, 'zero or one arguments');
} else if (node.arguments.length > 2) {
e.rune_invalid_arguments_length(node, rune, 'at most two arguments');
}
break;

@ -289,8 +289,8 @@ export function client_component(analysis, options) {
}
if (binding?.kind === 'state' || binding?.kind === 'raw_state') {
const value = binding.kind === 'state' ? b.call('$.proxy', b.id('$$value')) : b.id('$$value');
return [getter, b.set(alias ?? name, [b.stmt(b.call('$.set', b.id(name), value))])];
const call = b.call('$.set', b.id(name), b.id('$$value'), binding.kind === 'state' && b.true);
return [getter, b.set(alias ?? name, [b.stmt(call)])];
}
return getter;

@ -5,6 +5,7 @@ import * as b from '#compiler/builders';
import { get_rune } from '../../../scope.js';
import { should_proxy } from '../utils.js';
import { get_inspect_args } from '../../utils.js';
import { get_onchange, with_onchange } from './shared/state.js';
/**
* @param {CallExpression} node
@ -40,7 +41,11 @@ export function CallExpression(node, context) {
}
const callee = b.id('$.state', node.callee.loc);
return b.call(callee, value);
const onchange = get_onchange(
/** @type {Expression | undefined} */ (node.arguments[1]),
context
);
return with_onchange(b.call(callee, value), onchange);
}
case '$derived':

@ -14,6 +14,7 @@ import {
should_proxy
} from '../utils.js';
import { get_value } from './shared/declarations.js';
import { get_onchange, with_onchange } from './shared/state.js';
/**
* @param {VariableDeclaration} node
@ -130,6 +131,7 @@ export function VariableDeclaration(node, context) {
const args = /** @type {CallExpression} */ (init).arguments;
const value = /** @type {Expression} */ (args[0]) ?? b.void0; // TODO do we need the void 0? can we just omit it altogether?
const onchange = get_onchange(/** @type {Expression | undefined} */ (args[1]), context);
if (rune === '$state' || rune === '$state.raw') {
/**
@ -144,7 +146,8 @@ export function VariableDeclaration(node, context) {
const is_proxy = should_proxy(value, context.state.scope);
if (rune === '$state' && is_proxy) {
value = b.call('$.proxy', value);
// a proxy that is never reassigned has no source, so the callback attaches to the proxy itself
value = is_state ? b.call('$.proxy', value) : b.call('$.proxy', value, onchange);
if (dev && !is_state) {
value = b.call('$.tag_proxy', value, b.literal(id.name));
@ -158,6 +161,8 @@ export function VariableDeclaration(node, context) {
if (dev) {
value = b.call('$.tag', value, b.literal(id.name));
}
value = with_onchange(value, onchange);
}
return value;

@ -0,0 +1,34 @@
/** @import { Expression, Property } from 'estree' */
/** @import { ComponentContext, Context } from '../../types' */
import * as b from '#compiler/builders';
/**
* The `onchange` callback from a `$state` rune's options argument, if any
* @param {Expression | undefined} options the rune's second argument
* @param {ComponentContext | Context} context
* @returns {Expression | undefined}
*/
export function get_onchange(options, context) {
if (options?.type !== 'ObjectExpression') return;
const property = options.properties.find(
(property) =>
property.type === 'Property' &&
!property.computed &&
property.key.type === 'Identifier' &&
property.key.name === 'onchange'
);
if (property === undefined) return;
return /** @type {Expression} */ (context.visit(/** @type {Property} */ (property).value));
}
/**
* Wraps a `$.state(...)` call so `onchange` is registered on the source
* @param {Expression} call
* @param {Expression | undefined} onchange
*/
export function with_onchange(call, onchange) {
return onchange === undefined ? call : b.call('$.onchange', call, onchange);
}

@ -374,4 +374,6 @@ export interface Fork {
discard(): void;
}
export type { StateOptions } from './internal/client/types.js';
export * from './index-client.js';

@ -131,7 +131,15 @@ export {
user_effect,
user_pre_effect
} from './reactivity/effects.js';
export { mutable_source, mutate, set, state, update, update_pre } from './reactivity/sources.js';
export {
mutable_source,
mutate,
set,
state,
update,
update_pre,
onchange
} from './reactivity/sources.js';
export {
prop,
rest_props,

@ -28,7 +28,8 @@ import {
ASYNC,
WAS_MARKED,
CONNECTED,
REACTION_IS_UPDATING
REACTION_IS_UPDATING,
STATE_SYMBOL
} from '#client/constants';
import * as e from '../errors.js';
import { legacy_mode_flag, tracing_mode_flag } from '../../flags/index.js';
@ -42,7 +43,7 @@ import {
schedule_effect,
legacy_updates
} from './batch.js';
import { proxy } from '../proxy.js';
import { proxy, remove_onchange } from '../proxy.js';
import { execute_derived } from './deriveds.js';
import { set_signal_status, update_derived_status } from './status.js';
@ -189,6 +190,14 @@ export function set(source, value, should_proxy = false) {
*/
export function internal_set(source, value, updated_during_traversal = null) {
if (!source.equals(value)) {
var callback = source.o;
if (callback !== undefined) {
// the old tree stops reporting to this source's callback, the new one starts
remove_onchange(source.v, callback);
attach_onchange(value, callback);
}
if (is_destroying_effect) {
old_values.set(source, value);
} else if (!old_values.has(source)) {
@ -271,11 +280,51 @@ export function internal_set(source, value, updated_during_traversal = null) {
if (!batch.is_fork && eager_effects.size > 0 && eager_effects_deferred === 0) {
flush_eager_effects();
}
if (callback !== undefined) callback();
}
return value;
}
/**
* Registers `onchange` on a state source: it fires when the source is reassigned, and
* every proxy tree the source holds reports its mutations to it
* @template {Source} S
* @param {S} source
* @param {() => void} callback
* @returns {S}
*/
export function onchange(source, callback) {
var running = false;
// one guard per declaration, shared by the reassignment path and every proxy tree
// the source holds, so a callback that writes its own state runs once
source.o = () => {
if (running) return;
running = true;
try {
callback();
} finally {
running = false;
}
};
attach_onchange(source.v, source.o);
return source;
}
/**
* @param {unknown} value
* @param {() => void} callback
*/
function attach_onchange(value, callback) {
if (typeof value === 'object' && value !== null && STATE_SYMBOL in value) {
proxy(value, callback);
}
}
export function flush_eager_effects() {
try {
for (const effect of eager_effects) {

@ -15,6 +15,11 @@ export interface Signal {
wv: number;
}
export interface StateOptions {
/** Called synchronously whenever the state is reassigned or, for `$state`, mutated anywhere in its tree */
onchange?: () => void;
}
export interface Value<V = unknown> extends Signal {
/** Equality function */
equals: Equals;
@ -24,6 +29,8 @@ export interface Value<V = unknown> extends Signal {
rv: number;
/** The latest value for this signal */
v: V;
/** `onchange` callback, fired on reassignment and attached to every proxy tree this source holds */
o?: () => void;
// dev-only
/** A label (e.g. the `foo` in `let foo = $state(...)`) used for `$inspect.trace()` */

@ -3,6 +3,6 @@ import { test } from '../../test';
export default test({
error: {
code: 'rune_invalid_arguments_length',
message: '`$state` must be called with zero or one arguments'
message: '`$state` must be called with at most two arguments'
}
});

@ -3,6 +3,6 @@ import { test } from '../../test';
export default test({
error: {
code: 'rune_invalid_arguments_length',
message: '`$state.raw` must be called with zero or one arguments'
message: '`$state.raw` must be called with at most two arguments'
}
});

@ -0,0 +1,14 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target, logs }) {
const [btn, btn2] = target.querySelectorAll('button');
flushSync(() => btn.click());
assert.deepEqual(logs, ['foo', 'baz']);
flushSync(() => btn2.click());
assert.deepEqual(logs, ['foo', 'baz', 'foo', 'baz']);
}
});

@ -0,0 +1,15 @@
<script>
let foo = $state({ bar: 1 }, {
onchange(){
console.log("foo");
}
});
let baz = $state(foo, {
onchange(){
console.log("baz");
}
})
</script>
<button onclick={()=> foo.bar++}>foo</button>
<button onclick={()=> baz.bar++}>baz</button>

@ -0,0 +1,11 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target, logs }) {
const btn = target.querySelector('button');
flushSync(() => btn?.click());
assert.deepEqual(logs, [{ message: 'hello' }, { message: 'goodbye' }]);
}
});

@ -0,0 +1,14 @@
<script>
let object = $state(
{},
{
onchange() {
console.log($state.snapshot(object));
}
}
);
object.message = 'hello';
</script>
<button onclick={() => object.message = 'goodbye'}>goodbye</button>

@ -0,0 +1,14 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target, logs }) {
const [btn, btn2] = target.querySelectorAll('button');
flushSync(() => btn.click());
assert.deepEqual(logs, [[{}, {}, {}, {}, {}, {}, {}, {}]]);
flushSync(() => btn2.click());
assert.deepEqual(logs, [[{}, {}, {}, {}, {}, {}, {}, {}], []]);
}
});

@ -0,0 +1,14 @@
<script>
let array = $state([], {
onchange() {
console.log($state.snapshot(array));
}
});
</script>
<!-- clicking either of these buttons should result in at most one log -->
<button onclick={() => array = [{}, {}, {}, {}, {}, {}, {}, {}]}>populate array</button>
<button onclick={() => array.length = 0}>clear array</button>
<!-- without this, nested proxies aren't created -->
<pre>{JSON.stringify(array)}</pre>

@ -0,0 +1,17 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target, logs }) {
const [btn, btn2, btn3] = target.querySelectorAll('button');
flushSync(() => btn.click());
assert.deepEqual(logs, ['arr']);
flushSync(() => btn2.click());
assert.deepEqual(logs, ['arr', 'arr']);
flushSync(() => btn3.click());
assert.deepEqual(logs, ['arr', 'arr', 'arr']);
}
});

@ -0,0 +1,11 @@
<script>
let arr = $state([0,1,2], {
onchange(){
console.log("arr");
}
})
</script>
<button onclick={()=> arr.push(arr.length)}>push</button>
<button onclick={()=>arr.splice(0, 2)}>splice</button>
<button onclick={()=>arr.sort((a,b)=>b-a)}>sort</button>

@ -0,0 +1,11 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target, logs }) {
const btn = target.querySelector('button');
flushSync(() => btn?.click());
assert.deepEqual(logs, ['b changed']);
}
});

@ -0,0 +1,13 @@
<script>
let a = $state({});
let b = $state({ count: 0 }, {
onchange() {
console.log('b changed');
}
});
a.b = b;
</script>
<button onclick={()=> b.count++}>{b.count}</button>

@ -0,0 +1,64 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target, logs }) {
const [btn, btn2, btn3, btn4, btn5, btn6, btn7] = target.querySelectorAll('button');
assert.deepEqual(logs, [
'constructor count',
'constructor proxy',
'assign in constructor',
'assign in constructor proxy'
]);
logs.length = 0;
flushSync(() => btn.click());
assert.deepEqual(logs, ['class count']);
flushSync(() => btn2.click());
assert.deepEqual(logs, ['class count', 'class proxy']);
flushSync(() => btn3.click());
assert.deepEqual(logs, ['class count', 'class proxy', 'class proxy']);
flushSync(() => btn4.click());
assert.deepEqual(logs, [
'class count',
'class proxy',
'class proxy',
'declared in constructor'
]);
flushSync(() => btn5.click());
assert.deepEqual(logs, [
'class count',
'class proxy',
'class proxy',
'declared in constructor',
'declared in constructor'
]);
flushSync(() => btn6.click());
assert.deepEqual(logs, [
'class count',
'class proxy',
'class proxy',
'declared in constructor',
'declared in constructor',
'declared in constructor proxy'
]);
flushSync(() => btn7.click());
assert.deepEqual(logs, [
'class count',
'class proxy',
'class proxy',
'declared in constructor',
'declared in constructor',
'declared in constructor proxy',
'declared in constructor proxy'
]);
}
});

@ -0,0 +1,68 @@
<script>
class Test{
count = $state(0, {
onchange(){
console.log("class count");
}
})
proxy = $state({count: 0}, {
onchange(){
console.log("class proxy");
}
})
#in_constructor = $state(0, {
onchange(){
console.log("constructor count");
}
});
#in_constructor_proxy = $state({ count: 0 }, {
onchange(){
console.log("constructor proxy");
}
});
declared_in_constructor;
declared_in_constructor_proxy;
#assign_in_constructor;
#assign_in_constructor_proxy;
constructor(){
this.#in_constructor = 42;
this.#in_constructor_proxy.count++;
this.declared_in_constructor = $state(0, {
onchange(){
console.log("declared in constructor");
}
});
this.declared_in_constructor_proxy = $state({ count: 0 }, {
onchange(){
console.log("declared in constructor proxy");
}
});
this.#assign_in_constructor = $state(0, {
onchange(){
console.log("assign in constructor");
}
});
this.#assign_in_constructor++;
this.#assign_in_constructor_proxy = $state({ count: 0 }, {
onchange(){
console.log("assign in constructor proxy");
}
});
this.#assign_in_constructor_proxy.count++;
}
}
const class_test = new Test();
</script>
<button onclick={()=> class_test.count++}>{class_test.count}</button>
<button onclick={()=> class_test.proxy.count++}>{class_test.proxy.count}</button>
<button onclick={()=> class_test.proxy = {count: class_test.proxy.count+1}}>{class_test.proxy.count}</button>
<button onclick={()=> class_test.declared_in_constructor++}>{class_test.declared_in_constructor}</button>
<button onclick={()=> class_test.declared_in_constructor = class_test.declared_in_constructor + 1 }>{class_test.declared_in_constructor}</button>
<button onclick={()=> class_test.declared_in_constructor_proxy.count++}>{class_test.declared_in_constructor_proxy.count}</button>
<button onclick={()=> class_test.declared_in_constructor_proxy.count = class_test.declared_in_constructor_proxy.count + 1 }>{class_test.declared_in_constructor_proxy.count}</button>

@ -0,0 +1,20 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target, logs }) {
const [btn, btn2, btn3, btn4, btn5, btn6] = target.querySelectorAll('button');
logs.length = 0;
flushSync(() => btn.click());
flushSync(() => btn2.click());
flushSync(() => btn3.click());
flushSync(() => btn4.click());
flushSync(() => btn5.click());
assert.deepEqual(logs, []);
flushSync(() => btn6.click());
flushSync(() => btn.click());
assert.deepEqual(logs, ['arr', 'arr']);
}
});

@ -0,0 +1,43 @@
<script>
let arr = $state([{ count: 1 }, { count: 1 }], {
onchange(){
console.log("arr");
}
});
const item = arr.pop();
const item_2 = arr[0];
arr[0] = { count: 1 };
let obj = $state({ value: { count: 0 }, key: { count: 0 } }, {
onchange(){
console.log("obj");
}
});
const item_3 = obj.value;
delete obj.value;
const values = [...Object.values(obj)];
delete obj.key;
let arr_2 = $state([{ count: 1 }, { count: 1 }], {
onchange(){
console.log("arr_2");
}
});
const item_4 = arr_2[0];
arr_2.length = 0;
</script>
<button onclick={()=> item.count++}>{item.count}</button>
<button onclick={()=> item_2.count++}>{item_2.count}</button>
<button onclick={()=> item_3.count++}>{item_3.count}</button>
<button onclick={()=> values[0].count++}>{values[0].count}</button>
<button onclick={()=> item_4.count++}>{item_4.count}</button>
<button onclick={()=> arr.push(item)}>push</button>

@ -0,0 +1,14 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target, logs }) {
const [btn, btn2] = target.querySelectorAll('button');
flushSync(() => btn.click());
assert.deepEqual(logs, ['proxy']);
flushSync(() => btn2.click());
assert.deepEqual(logs, ['proxy', 'proxy']);
}
});

@ -0,0 +1,10 @@
<script>
let proxy = $state({count: 0}, {
onchange(){
console.log("proxy");
}
})
</script>
<button onclick={()=> proxy.count++}>{proxy.count}</button>
<button onclick={()=> proxy = {count: proxy.count+1}}>{proxy.count}</button>

@ -0,0 +1,20 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target, logs }) {
const [btn, btn2, btn3, btn4] = target.querySelectorAll('button');
flushSync(() => btn.click());
assert.deepEqual(logs, ['a']);
flushSync(() => btn2.click());
assert.deepEqual(logs, ['a', 'b', 'c']);
flushSync(() => btn3.click());
assert.deepEqual(logs, ['a', 'b', 'c', 'b', 'c']);
flushSync(() => btn4.click());
assert.deepEqual(logs, ['a', 'b', 'c', 'b', 'c', 'c']);
flushSync(() => btn2.click());
assert.deepEqual(logs, ['a', 'b', 'c', 'b', 'c', 'c', 'b']);
}
});

@ -0,0 +1,27 @@
<script>
let obj = { count: 0 };
let a = $state(obj, {
onchange() {
console.log('a');
}
});
let b = $state(obj, {
onchange() {
console.log('b');
}
});
let c = $state(b, {
onchange() {
console.log('c');
}
});
</script>
<button onclick={()=> a.count++}>{a.count}</button>
<button onclick={()=> b.count++}>{b.count}</button>
<button onclick={()=> c.count++}>{c.count}</button>
<button onclick={() => c = { count: c.count }}>unlink</button>

@ -0,0 +1,17 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target, logs }) {
const [count, items] = target.querySelectorAll('button');
flushSync(() => count.click());
assert.deepEqual(logs, ['count', 20]);
assert.htmlEqual(count.innerHTML, '10');
logs.length = 0;
flushSync(() => items.click());
assert.deepEqual(logs, ['items', 3]);
assert.htmlEqual(items.innerHTML, '2');
}
});

@ -0,0 +1,18 @@
<script>
let count = $state(0, {
onchange() {
console.log('count', count);
count = Math.min(count, 10);
}
});
let items = $state([], {
onchange() {
console.log('items', items.length);
if (items.length > 2) items.length = 2;
}
});
</script>
<button onclick={() => (count = 20)}>{count}</button>
<button onclick={() => items.push(1, 2, 3)}>{items.length}</button>

@ -0,0 +1,11 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target, logs }) {
const btn = target.querySelector('button');
flushSync(() => btn?.click());
assert.deepEqual(logs, ['count']);
}
});

@ -0,0 +1,9 @@
<script>
let count = $state(0, {
onchange(){
console.log("count");
}
})
</script>
<button onclick={()=> count++}>{count}</button>

@ -0,0 +1,106 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target, logs }) {
const [btn, btn2, btn3, btn4, btn5, btn6, btn7, btn8, btn9, btn10, btn11, btn12, btn13] =
target.querySelectorAll('button');
assert.deepEqual(logs, [
'constructor count',
'constructor object',
'assign in constructor',
'assign in constructor object'
]);
logs.length = 0;
flushSync(() => btn.click());
assert.deepEqual(logs, ['count']);
flushSync(() => btn2.click());
assert.deepEqual(logs, ['count']);
flushSync(() => btn3.click());
assert.deepEqual(logs, ['count', 'object']);
flushSync(() => btn4.click());
assert.deepEqual(logs, ['count', 'object', 'class count']);
flushSync(() => btn5.click());
assert.deepEqual(logs, ['count', 'object', 'class count']);
flushSync(() => btn6.click());
assert.deepEqual(logs, ['count', 'object', 'class count', 'class object']);
flushSync(() => btn7.click());
assert.deepEqual(logs, [
'count',
'object',
'class count',
'class object',
'declared in constructor'
]);
flushSync(() => btn8.click());
assert.deepEqual(logs, [
'count',
'object',
'class count',
'class object',
'declared in constructor',
'declared in constructor object'
]);
flushSync(() => btn9.click());
assert.deepEqual(logs, [
'count',
'object',
'class count',
'class object',
'declared in constructor',
'declared in constructor object'
]);
flushSync(() => btn10.click());
assert.deepEqual(logs, [
'count',
'object',
'class count',
'class object',
'declared in constructor',
'declared in constructor object'
]);
flushSync(() => btn11.click());
assert.deepEqual(logs, [
'count',
'object',
'class count',
'class object',
'declared in constructor',
'declared in constructor object'
]);
flushSync(() => btn12.click());
assert.deepEqual(logs, [
'count',
'object',
'class count',
'class object',
'declared in constructor',
'declared in constructor object'
]);
flushSync(() => btn13.click());
assert.deepEqual(logs, [
'count',
'object',
'class count',
'class object',
'declared in constructor',
'declared in constructor object',
'arr'
]);
}
});

@ -0,0 +1,94 @@
<script>
let count = $state.raw(0, {
onchange(){
console.log("count");
}
})
let object = $state.raw({count: 0}, {
onchange(){
console.log("object");
}
})
class Test{
count = $state.raw(0, {
onchange(){
console.log("class count");
}
})
object = $state.raw({count: 0}, {
onchange(){
console.log("class object");
}
})
#in_constructor = $state.raw(0, {
onchange(){
console.log("constructor count");
}
});
#in_constructor_obj = $state.raw({ count: 0 }, {
onchange(){
console.log("constructor object");
}
});
declared_in_constructor;
declared_in_constructor_obj;
#assign_in_constructor;
#assign_in_constructor_obj;
constructor(){
this.#in_constructor++;
this.#in_constructor_obj = { count: this.#in_constructor_obj.count + 1 };
this.declared_in_constructor = $state.raw(0, {
onchange(){
console.log("declared in constructor");
}
});
this.declared_in_constructor_obj = $state.raw({ count: 0 }, {
onchange(){
console.log("declared in constructor object");
}
});
this.#assign_in_constructor = $state.raw(0, {
onchange(){
console.log("assign in constructor");
}
});
this.#assign_in_constructor++;
this.#assign_in_constructor_obj = $state.raw({ count: 0 }, {
onchange(){
console.log("assign in constructor object");
}
});
this.#assign_in_constructor_obj = { count: this.#assign_in_constructor_obj.count + 1 };
}
}
const class_test = new Test();
let arr = $state.raw([0,1,2], {
onchange(){
console.log("arr");
}
})
</script>
<button onclick={()=> count++}>{count}</button>
<button onclick={()=> object.count++}>{object.count}</button>
<button onclick={()=> object = {count: object.count+1}}>{object.count}</button>
<button onclick={()=> class_test.count++}>{class_test.count}</button>
<button onclick={()=> class_test.object.count++}>{class_test.object.count}</button>
<button onclick={()=> class_test.object = {count: class_test.object.count+1}}>{class_test.object.count}</button>
<button onclick={()=> class_test.declared_in_constructor++}>{class_test.declared_in_constructor}</button>
<button onclick={()=> class_test.declared_in_constructor_obj = {count: class_test.declared_in_constructor_obj.count + 1}}>{class_test.declared_in_constructor_obj.count}</button>
<button onclick={()=> class_test.declared_in_constructor_obj.count++}>{class_test.declared_in_constructor_obj.count}</button>
<button onclick={()=> arr.push(arr.length)}>push</button>
<button onclick={()=>arr.splice(0, 2)}>splice</button>
<button onclick={()=>arr.sort((a,b)=>b-a)}>sort</button>
<button onclick={()=>arr = []}>assign</button>

@ -458,16 +458,9 @@ declare module 'svelte' {
* @deprecated Use [`$effect`](https://svelte.dev/docs/svelte/$effect) instead
* */
export function afterUpdate(fn: () => void): void;
export function hydratable<T>(key: string, fn: () => T): T;
/**
* Create a snippet programmatically
* */
export function createRawSnippet<Params extends unknown[]>(fn: (...params: Getters<Params>) => {
render: () => string;
setup?: (element: Element) => void | (() => void);
}): Snippet<Params>;
/** Anything except a function */
type NotFunction<T> = T extends Function ? never : T;
type Getters<T> = {
[K in keyof T]: () => T[K];
};
/**
* Synchronously flush any pending updates.
* Returns void if no callback is provided, otherwise returns the result of calling the callback.
@ -491,6 +484,20 @@ declare module 'svelte' {
* @since 5.42
*/
export function fork(fn: () => void): Fork;
export interface StateOptions {
/** Called synchronously whenever the state is reassigned or, for `$state`, mutated anywhere in its tree */
onchange?: () => void;
}
export function hydratable<T>(key: string, fn: () => T): T;
/**
* Create a snippet programmatically
* */
export function createRawSnippet<Params extends unknown[]>(fn: (...params: Getters<Params>) => {
render: () => string;
setup?: (element: Element) => void | (() => void);
}): Snippet<Params>;
/** Anything except a function */
type NotFunction<T> = T extends Function ? never : T;
/**
* Returns a `[get, set, has]` triplet of functions for working with context in a type-safe way.
*
@ -605,9 +612,6 @@ declare module 'svelte' {
* ```
* */
export function untrack<T>(fn: () => T): T;
type Getters<T> = {
[K in keyof T]: () => T[K];
};
export {};
}
@ -3338,6 +3342,11 @@ declare module 'svelte/types/compiler/interfaces' {
*
* @param initial The initial value
*/
declare function $state<T>(
initial: undefined,
options?: import('svelte').StateOptions
): T | undefined;
declare function $state<T>(initial: T, options?: import('svelte').StateOptions): T;
declare function $state<T>(initial: T): T;
declare function $state<T>(): T | undefined;
@ -3448,6 +3457,11 @@ declare namespace $state {
*
* @param initial The initial value
*/
export function raw<T>(
initial: undefined,
options?: import('svelte').StateOptions
): T | undefined;
export function raw<T>(initial?: T, options?: import('svelte').StateOptions): T;
export function raw<T>(initial: T): T;
export function raw<T>(): T | undefined;
/**

Loading…
Cancel
Save