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

Управление состоянием

Если вы привыкли создавать клиентские приложения, управление состоянием в приложении, которое работает и на сервере, и на клиенте, может показаться сложным. В этом разделе приведены советы, как избежать некоторых распространённых проблем.

Браузеры отслеживают состояние — оно сохраняется в памяти по мере взаимодействия пользователя с приложением. Серверы же, напротив, не имеют состояния — содержимое ответа полностью определяется содержимым запроса.

По крайней мере, в теории. На практике серверы часто работают длительное время и используются множеством пользователей. Поэтому важно не хранить данные в общих переменных. Например, рассмотрим этот код:

+page.server.js
let user;
/** @type {import('./$types').PageServerLoad} */
export function load() {
return { user };
}
/** @satisfies {import('./$types').Actions} */
export const actions = {
default: async ({ request }) => {
const data = await request.formData();
// НЕ ДЕЛАЙТЕ ТАК!
user = {
name: data.get('name'),
embarrassingSecret: data.get('secret')
};
}
}

Переменная user является общей для всех, кто подключается к этому серверу. Если Алиса отправит неловкий секрет, а Боб посетит страницу после неё, Боб узнает секрет Алисы. Кроме того, когда Алиса вернётся на сайт позже в тот же день, сервер может перезапуститься, и её данные будут потеряны.

Вместо этого вам следует аутентифицировать пользователя с помощью куки и сохранять данные в базе данных.

По той же причине ваши функции load должны быть чистыми — без побочных эффектов (за исключением разве что случайных console.log(...)). Например, у вас может возникнуть соблазн записать данные в хранилище или глобальное состояние внутри функции load, чтобы затем использовать это значение в ваших компонентах:

+page.js
import { user } from '$lib/user';
/** @type {import('./$types').PageLoad} */
export async function load({ fetch }) {
const response = await fetch('/api/user');
// НЕ ДЕЛАЙТЕ ТАК!
user.set(await response.json());
}

Как и в предыдущем примере, это помещает информацию одного пользователя в место, доступное всем пользователям. Вместо этого просто возвращайте данные…

+page.js
/** @type {import('./$types').PageServerLoad} */
export async function load({ fetch }) {
const response = await fetch('/api/user');
return {
user: await response.json()
};
}

…и передавайте их в компоненты, которым они нужны, или используйте page.data.

Если вы не используете SSR (рендеринг на стороне сервера), то нет риска случайно предоставить данные одного пользователя другому. Однако вам всё равно следует избегать побочных эффектов в функциях load — ваше приложение станет гораздо проще для понимания без них.

Использование состояния и хранилищ с контекстом

Заголовок раздела «Использование состояния и хранилищ с контекстом»

Возможно, вам интересно, как мы можем использовать page.data и другие состояния приложения (или хранилища приложения), если глобальное состояние использовать нельзя. Ответ в том, что состояние и хранилища приложения на сервере используют Context API Svelte — состояние (или хранилище) прикрепляется к дереву компонентов с помощью setContext, а при подписке извлекается через getContext. Мы можем сделать то же самое с нашим собственным состоянием:

src/routes/+layout.svelte
<script>
import { setContext } from 'svelte';
/** @type {import('./$types').LayoutProps} */
let { data } = $props();
// Передаем функцию, ссылающуюся на наше состояние,
// в контекст для доступа дочерних компонентов
setContext('user', () => data.user);
</script>
src/routes/user/+page.svelte
<script>
import { getContext } from 'svelte';
// Получаем хранилище пользователя из контекста
const user = getContext('user');
</script>
<p>Приветствуем, {user().name}</p>

Обновление значения состояния на основе контекста на страницах или в компонентах более глубокого уровня во время рендеринга через SSR не повлияет на значение в родительском компоненте, так как он уже был отрендерен к моменту обновления значения состояния. В отличие от этого, на клиенте (когда включен CSR, что является значением по умолчанию) значение будет распространяться, и компоненты, страницы и макеты выше в иерархии будут реагировать на новое значение. Поэтому, чтобы избежать «мигания» значений во время обновления состояния при гидратации, обычно рекомендуется передавать состояние вниз в компоненты, а не вверх.

Если вы не используете SSR (и можете гарантировать, что не будете использовать SSR в будущем), то можете безопасно хранить состояние в общем модуле, не используя Context API.

Состояние компонентов и страниц сохраняется

Заголовок раздела «Состояние компонентов и страниц сохраняется»

Когда вы перемещаетесь по приложению, SvelteKit повторно использует существующие компоненты макетов и страниц. Например, если у вас есть маршрут вида…

src/routes/blog/[slug]/+page.svelte
<script>
/** @type {import('./$types').PageProps} */
let { data } = $props();
// ЭТОТ КОД СОДЕРЖИТ ОШИБКИ!
const wordCount = data.content.split(' ').length;
const estimatedReadingTime = wordCount / 250;
</script>
<header>
<h1>{data.title}</h1>
<p>Время чтения: {Math.round(estimatedReadingTime)} мин.</p>
</header>
<div>{@html data.content}</div>

…то переход с /blog/my-short-post на /blog/my-long-post не приведет к уничтожению и повторному созданию макета, страницы и любых других компонентов. Вместо этого проп data (а следовательно, data.title и data.content) обновится (как и в любом другом компоненте Svelte), и, поскольку код не выполняется заново, методы жизненного цикла, такие как onMount и onDestroy, не запустятся снова, а estimatedReadingTime не будет пересчитан.

Вместо этого нам нужно сделать значение реактивным:

src/routes/blog/[slug]/+page.svelte
<script>
/** @type {import('./$types').PageProps} */
let { data } = $props();
let wordCount = $derived(data.content.split(' ').length);
let estimatedReadingTime = $derived(wordCount / 250);
</script>

Повторное использование компонентов таким образом означает, что такие вещи как состояние прокрутки боковой панели сохраняются, и вы можете легко анимировать изменения значений. В случае, если вам действительно нужно полностью уничтожить и пересоздать компонент при навигации, вы можете использовать этот подход:

<script>
import { page } from '$app/state';
</script>
{#key page.url.pathname}
<BlogPost title={data.title} content={data.title} />
{/key}

Если у вас есть состояние, которое должно сохраняться при перезагрузке и/или влиять на SSR (например, фильтры или правила сортировки в таблице), параметры URL (например, ?sort=price&order=ascending) — это хорошее место для их хранения. Вы можете поместить их в атрибуты <a href="..."> или <form action="...">, либо установить программно через goto('?key=value'). Доступ к ним можно получить внутри функций load через параметр url, а в компонентах — через page.url.searchParams.

Некоторые состояния интерфейса, такие как «открыт ли аккордеон?», являются временными — если пользователь уйдёт со страницы или обновит её, не страшно, если состояние будет потеряно. В некоторых случаях вам нужно, чтобы данные сохранялись, если пользователь перейдёт на другую страницу и вернётся обратно, но хранение состояния в URL или базе данных было бы излишним. Для этого SvelteKit предоставляет снимки состояния, которые позволяют связать состояние компонента с записью в истории.