Перейти к содержимому

$state

Руна $state позволяет вам создавать реактивное состояние, что означает, что ваш пользовательский интерфейс реагирует на изменения.

<script>
let count = $state(0);
</script>
<button onclick={() => count++}>
нажатий: {count}
</button>

В отличие от других фреймворков, с которыми вы могли столкнуться, нет API для взаимодействия с состоянием — count просто число, а не объект или функция, и вы можете обновлять его так же, как обновляете любую другую переменную.

Если $state используется с массивом или простым объектом, результатом будет глубоко реактивный прокси состояния. Объекты Прокси позволяют Svelte выполнять код при чтении или записи свойств, включая методы, такие как array.push(...), что вызывает детализированные обновления.

Состояние проксируется рекурсивно, пока Svelte не встретит что-то, отличное от массива или простого объекта (например, класс). В таком случае…

let todos = $state([
{
done: false,
text: 'добавить задачу'
}
]);

…изменение свойства отдельной задачи вызовет обновление всего в вашем пользовательском интерфейсе, что зависит от этого конкретного свойства:

todos[0].done = !todos[0].done;

Если вы добавите новый объект в массив, он также будет проксирован:

todos.push({
done: false,
text: 'пообедать'
});

Обратите внимание, что если вы деструктурируете реактивное значение, ссылки не являются реактивными — как и в обычном JavaScript, они анализируются в момент деструктуризации:

let { done, text } = todos[0];
// это не повлияет на значение `done`
todos[0].done = !todos[0].done;

Экземпляры классов не проксируются. Вместо этого вы можете использовать $state в полях класса (как публичных, так и приватных) или в качестве первого присваивания свойства непосредственно внутри конструктора:

class Todo {
done = $state(false);
constructor(text) {
this.text = $state(text);
}
reset() {
this.text = '';
this.done = false;
}
}

При вызове методов в JavaScript значение this имеет значение. Это не сработает, потому что this внутри метода reset будет ссылаться на <button>, а не на Todo:

<button onclick={todo.reset}>
reset
</button>

Вы можете использовать либо встроенную функцию…

<button onclick={() => todo.reset()}>
reset
</button>

…либо стрелочную функцию в определении класса:

class Todo {
done = $state(false);
text = $state();
constructor(text) {
this.text = text;
}
reset = () => {
this.text = '';
this.done = false;
}
}

Svelte предоставляет реактивные реализации встроенных классов, таких как Set и Map, которые можно импортировать из svelte/reactivity.

В случаях, когда вы не хотите, чтобы объекты и массивы были глубоко реактивными, вы можете использовать $state.raw.

Состояние, объявленное с помощью $state.raw, не может быть мутировано; его можно только переопределить. Другими словами, вместо того чтобы присваивать значение свойству объекта или использовать метод массива, такой как push, замените объект или массив целиком, если хотите его обновить:

let person = $state.raw({
name: 'Heraclitus',
age: 49
});
// это не окажет никакого эффекта
person.age += 1;
// это сработает, потому что мы создаем новый объект
person = {
name: 'Heraclitus',
age: 50
};

Это может улучшить производительность при работе с большими массивами и объектами, которые вы не планировали мутировать, поскольку это позволяет избежать затрат на их реактивность. Обратите внимание, что исходное состояние может содержать реактивное состояние (например, исходный массив реактивных объектов).

Как и в случае со $state, вы можете объявлять поля класса, используя $state.raw.

Чтобы сделать статический снимок глубоко реактивного прокси $state, используйте $state.snapshot:

<script>
let counter = $state({ count: 0 });
function onclick() {
// Будет выведено `{ count: ... }`, а не `Proxy { ... }`
console.log($state.snapshot(counter));
}
</script>

Это удобно, когда вы хотите передать состояние во внешнюю библиотеку или API, которые не ожидают прокси, такие как structuredClone.

JavaScript — это язык с передачей по значению — когда вы вызываете функцию, аргументы являются значениями, а не переменными. Другими словами:

index.js
/**
* @param {number} a
* @param {number} b
*/
function add(a, b) {
return a + b;
}
let a = 1;
let b = 2;
let total = add(a, b);
console.log(total); // 3
a = 3;
b = 4;
console.log(total); // всё ещё 3!

Если add хочет получить доступ к текущим значениям a и b, а также вернуть текущее значение total, вам нужно использовать функции вместо этого:

index.js
/**
* @param {() => number} getA
* @param {() => number} getB
*/
function add(getA, getB) {
return () => getA() + getB();
}
let a = 1;
let b = 2;
let total = add(() => a, () => b);
console.log(total()); // 3
a = 3;
b = 4;
console.log(total()); // 7

Состояние в Svelte ничем не отличается — когда вы ссылаетесь на что-то, объявленное с помощью руны $state

let a = $state(1);
let b = $state(2);

…вы получаете его текущее значение.

Обратите внимание, что «функции» — это широкое понятие — оно охватывает свойства прокси и свойства get/set

index.js
/**
* @param {{ a: number, b: number }} input
*/
function add(input) {
return {
get value() {
return input.a + input.b;
}
};
}
let input = $state({ a: 1, b: 2 });
let total = add(input);
console.log(total.value); // 3
input.a = 3;
input.b = 4;
console.log(total.value); // 7

…хотя если вы обнаружите, что пишете код подобным образом, подумайте о том, чтобы перейти на классы.

Вы можете объявлять состояние в файлах .svelte.js и .svelte.ts, но экспортировать это состояние можно только если оно не переназначается напрямую. Другими словами, так делать нельзя:

state.svelte.js
export let count = $state(0);
export function increment() {
count += 1;
}

Это происходит потому, что все ссылки на count преобразуются компилятором Svelte — приведённый выше код примерно эквивалентен следующему:

state.svelte.js (вывод компилятора)
export let count = $.state(0);
export function increment() {
$.set(count, $.get(count) + 1);
}

Поскольку компилятор обрабатывает только один файл за раз, если другой файл импортирует count, Svelte не знает, что нужно обернуть каждое обращение в $.get и $.set:

import { count } from './state.svelte.js';
console.log(typeof count); // 'object', не 'number'

Это оставляет вам два варианта для совместного использования состояния между модулями — либо не переназначать его…

// Это разрешено — поскольку мы обновляем
// `counter.count`, а не сам `counter`,
// Svelte не оборачивает его в `$.state`
export const counter = $state({
count: 0
});
export function increment() {
counter.count += 1;
}

…либо не экспортировать его напрямую:

let count = $state(0);
export function getCount() {
return count;
}
export function increment() {
count += 1;
}