メインコンテンツへスキップ

状態管理

クライアントのみのアプリケーションの構築に慣れている場合、サーバーとクライアントにまたがるアプリケーションの状態管理は、最初は難しいように見えるかもしれません。このセクションでは、いくつかのよくある問題を回避するためのヒントを示します。

サーバーでの共有状態を避ける

ブラウザは状態を持つ—状態は、ユーザーがアプリケーションと対話する際にメモリに保存されます。一方、サーバーは状態を持たない—レスポンスの内容は、リクエストの内容によって完全に決定されます。

概念的にはそうです。実際には、サーバーはしばしば長期間稼働し、複数のユーザーによって共有されます。そのため、共有変数にデータを保存しないことが重要です。たとえば、次のコードを考えてみましょう。

+page.server
let let user: anyuser;

/** @type {import('./$types').PageServerLoad} */
export function 
function load(): {
    user: any;
}
@type{import('./$types').PageServerLoad}
load
() {
return { user: anyuser }; } /** @satisfies {import('./$types').Actions} */ export const
const actions: {
    default: ({ request }: {
        request: any;
    }) => Promise<void>;
}
@satisfies{import('./$types').Actions}
actions
= {
default: ({ request }: {
    request: any;
}) => Promise<void>
default
: async ({ request: anyrequest }) => {
const const data: anydata = await request: anyrequest.formData(); // NEVER DO THIS! let user: anyuser = { name: anyname: const data: anydata.get('name'), embarrassingSecret: anyembarrassingSecret: const data: anydata.get('secret') }; } }
import type { 
type PageServerLoad = (event: Kit.ServerLoadEvent<Record<string, any>, Record<string, any>, string | null>) => MaybePromise<void | Record<string, any>>
type PageServerLoad = (event: Kit.ServerLoadEvent<Record<string, any>, Record<string, any>, string | null>) => MaybePromise<void | Record<string, any>>
PageServerLoad
,
type Actions = {
    [x: string]: Kit.Action<Record<string, any>, void | Record<string, any>, string | null>;
}
type Actions = {
    [x: string]: Kit.Action<Record<string, any>, void | Record<string, any>, string | null>;
}
Actions
} from './$types';
let let user: anyuser; export const const load: PageServerLoadload:
type PageServerLoad = (event: Kit.ServerLoadEvent<Record<string, any>, Record<string, any>, string | null>) => MaybePromise<void | Record<string, any>>
type PageServerLoad = (event: Kit.ServerLoadEvent<Record<string, any>, Record<string, any>, string | null>) => MaybePromise<void | Record<string, any>>
PageServerLoad
= () => {
return { user: anyuser }; }; export const
const actions: {
    default: ({ request }: Kit.RequestEvent<Record<string, any>, string | null>) => Promise<void>;
}
actions
= {
default: ({ request }: Kit.RequestEvent<Record<string, any>, string | null>) => Promise<void>default: async ({ request: Request

The original request object

request
}) => {
const const data: FormDatadata = await request: Request

The original request object

request
.Body.formData(): Promise<FormData>formData();
// NEVER DO THIS! let user: anyuser = { name: FormDataEntryValue | nullname: const data: FormDatadata.FormData.get(name: string): FormDataEntryValue | nullget('name'), embarrassingSecret: FormDataEntryValue | nullembarrassingSecret: const data: FormDatadata.FormData.get(name: string): FormDataEntryValue | nullget('secret') }; } } satisfies
type Actions = {
    [x: string]: Kit.Action<Record<string, any>, void | Record<string, any>, string | null>;
}
type Actions = {
    [x: string]: Kit.Action<Record<string, any>, void | Record<string, any>, string | null>;
}
Actions

user変数は、このサーバーに接続するすべてのユーザーによって共有されます。Aliceが恥ずかしい秘密を送信し、Bobがその後ページにアクセスした場合、BobはAliceの秘密を知ることになります。さらに、Aliceがその日の後半にサイトに戻った場合、サーバーが再起動され、データが失われる可能性があります。

代わりに、cookiesを使用してユーザーを認証し、データをデータベースに永続化する必要があります。

load関数での副作用を避ける

同じ理由で、load関数は純粋である必要があります—副作用はありません(場合によってはconsole.log(...)を除く)。たとえば、コンポーネントでストアの値を使用できるように、load関数内でストアに書き込もうとする場合があります。

+page
import { 
const user: {
    set: (value: any) => void;
}
user
} from '$lib/user';
/** @type {import('./$types').PageLoad} */ export async function function load(event: LoadEvent<Record<string, any>, Record<string, any> | null, Record<string, any>, string | null>): MaybePromise<void | Record<string, any>>
@type{import('./$types').PageLoad}
load
({
fetch: {
    (input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
    (input: string | URL | globalThis.Request, init?: RequestInit): Promise<Response>;
}

fetch is equivalent to the native fetch web API, with a few additional features:

  • It can be used to make credentialed requests on the server, as it inherits the cookie and authorization headers for the page request.
  • It can make relative requests on the server (ordinarily, fetch requires a URL with an origin when used in a server context).
  • Internal requests (e.g. for +server.js routes) go directly to the handler function when running on the server, without the overhead of an HTTP call.
  • During server-side rendering, the response will be captured and inlined into the rendered HTML by hooking into the text and json methods of the Response object. Note that headers will not be serialized, unless explicitly included via filterSerializedResponseHeaders
  • During hydration, the response will be read from the HTML, guaranteeing consistency and preventing an additional network request.

You can learn more about making credentialed requests with cookies here

fetch
}) {
const const response: Responseresponse = await fetch: (input: string | URL | globalThis.Request, init?: RequestInit) => Promise<Response> (+1 overload)fetch('/api/user'); // NEVER DO THIS!
const user: {
    set: (value: any) => void;
}
user
.set: (value: any) => voidset(await const response: Responseresponse.Body.json(): Promise<any>json());
}
import { 
const user: {
    set: (value: any) => void;
}
user
} from '$lib/user';
import type { type PageLoad = (event: LoadEvent<Record<string, any>, Record<string, any> | null, Record<string, any>, string | null>) => MaybePromise<void | Record<string, any>>PageLoad } from './$types'; export const const load: PageLoadload: type PageLoad = (event: LoadEvent<Record<string, any>, Record<string, any> | null, Record<string, any>, string | null>) => MaybePromise<void | Record<string, any>>PageLoad = async ({
fetch: {
    (input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
    (input: string | URL | globalThis.Request, init?: RequestInit): Promise<Response>;
}

fetch is equivalent to the native fetch web API, with a few additional features:

  • It can be used to make credentialed requests on the server, as it inherits the cookie and authorization headers for the page request.
  • It can make relative requests on the server (ordinarily, fetch requires a URL with an origin when used in a server context).
  • Internal requests (e.g. for +server.js routes) go directly to the handler function when running on the server, without the overhead of an HTTP call.
  • During server-side rendering, the response will be captured and inlined into the rendered HTML by hooking into the text and json methods of the Response object. Note that headers will not be serialized, unless explicitly included via filterSerializedResponseHeaders
  • During hydration, the response will be read from the HTML, guaranteeing consistency and preventing an additional network request.

You can learn more about making credentialed requests with cookies here

fetch
}) => {
const const response: Responseresponse = await fetch: (input: string | URL | globalThis.Request, init?: RequestInit) => Promise<Response> (+1 overload)fetch('/api/user'); // NEVER DO THIS!
const user: {
    set: (value: any) => void;
}
user
.set: (value: any) => voidset(await const response: Responseresponse.Body.json(): Promise<any>json());
};

前の例と同様に、これはあるユーザーの情報がすべてのユーザーによって共有される場所に配置されます。代わりに、データを返すだけです…

+page
/** @type {import('./$types').PageServerLoad} */
export async function 
function load({ fetch }: {
    fetch: any;
}): Promise<{
    user: any;
}>
@type{import('./$types').PageServerLoad}
load
({ fetch: anyfetch }) {
const const response: anyresponse = await fetch: anyfetch('/api/user'); return { user: anyuser: await const response: anyresponse.json() }; }
import type { 
type PageServerLoad = (event: Kit.ServerLoadEvent<Record<string, any>, Record<string, any>, string | null>) => MaybePromise<void | Record<string, any>>
type PageServerLoad = (event: Kit.ServerLoadEvent<Record<string, any>, Record<string, any>, string | null>) => MaybePromise<void | Record<string, any>>
PageServerLoad
} from './$types';
export const const load: PageServerLoadload:
type PageServerLoad = (event: Kit.ServerLoadEvent<Record<string, any>, Record<string, any>, string | null>) => MaybePromise<void | Record<string, any>>
type PageServerLoad = (event: Kit.ServerLoadEvent<Record<string, any>, Record<string, any>, string | null>) => MaybePromise<void | Record<string, any>>
PageServerLoad
= async ({
fetch: {
    (input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
    (input: string | URL | globalThis.Request, init?: RequestInit): Promise<Response>;
}

fetch is equivalent to the native fetch web API, with a few additional features:

  • It can be used to make credentialed requests on the server, as it inherits the cookie and authorization headers for the page request.
  • It can make relative requests on the server (ordinarily, fetch requires a URL with an origin when used in a server context).
  • Internal requests (e.g. for +server.js routes) go directly to the handler function when running on the server, without the overhead of an HTTP call.
  • During server-side rendering, the response will be captured and inlined into the rendered HTML by hooking into the text and json methods of the Response object. Note that headers will not be serialized, unless explicitly included via filterSerializedResponseHeaders
  • During hydration, the response will be read from the HTML, guaranteeing consistency and preventing an additional network request.

You can learn more about making credentialed requests with cookies here

fetch
}) => {
const const response: Responseresponse = await fetch: (input: string | URL | globalThis.Request, init?: RequestInit) => Promise<Response> (+1 overload)fetch('/api/user'); return { user: anyuser: await const response: Responseresponse.Body.json(): Promise<any>json() }; };

…そして、それを必要とするコンポーネントに渡すか、$page.dataを使用します。

SSRを使用していない場合、誤ってあるユーザーのデータを別のユーザーに公開するリスクはありません。しかし、load関数での副作用は依然として避けるべきです—アプリケーションはそれらがない方がはるかに簡単に理解できます。

コンテキストでのストアの使用

独自のストアを使用できない場合、どのように$page.dataや他のアプリストアを使用できるのか疑問に思うかもしれません。答えは、サーバー上のアプリストアがSvelteのコンテキストAPIを使用していることです—ストアはsetContextを使用してコンポーネントツリーにアタッチされ、サブスクライブするとgetContextを使用して取得します。独自のストアでも同じことができます。

src/routes/+layout
<script>
	import { setContext } from 'svelte';
	import { writable } from 'svelte/store';

	/** @type {{ data: import('./$types').LayoutData }} */
	let { data } = $props();

	// Create a store and update it when necessary...
	const user = writable(data.user);
	$effect.pre(() => {
		user.set(data.user);
	});

	// ...and add it to the context for child components to access
	setContext('user', user);
</script>
<script lang="ts">
	import { setContext } from 'svelte';
	import { writable } from 'svelte/store';
	import type { LayoutData } from './$types';

	let { data }: { data: LayoutData } = $props();

	// Create a store and update it when necessary...
	const user = writable(data.user);
	$effect.pre(() => {
		user.set(data.user);
	});

	// ...and add it to the context for child components to access
	setContext('user', user);
</script>
src/routes/user/+page
<script>
	import { getContext } from 'svelte';

	// Retrieve user store from context
	const user = getContext('user');
</script>

<p>Welcome {$user.name}</p>

SSRを介してページがレンダリングされている間に、より深いレベルのページまたはコンポーネントでコンテキストベースのストアの値を更新しても、ストアの値が更新されるまでに親コンポーネントは既にレンダリングされているため、親コンポーネントの値には影響しません。対照的に、クライアント側(CSRが有効になっている場合、デフォルトでは有効になっています)では、値が伝播され、階層の上位にあるコンポーネント、ページ、レイアウトは新しい値に反応します。したがって、ハイドレーション中の状態更新中に値が「点滅」するのを避けるために、一般的に状態をコンポーネントに渡す方が、コンポーネントから上に渡すよりも推奨されます。

SSRを使用しておらず(将来SSRを使用する必要がないことを保証できる場合)、コンテキストAPIを使用せずに、共有モジュールに状態を安全に保持できます。

コンポーネントとページの状態は保持される

アプリケーション内を移動すると、SvelteKitは既存のレイアウトとページコンポーネントを再利用します。たとえば、次のようなルートがあるとします…

src/routes/blog/[slug]/+page
<script>
	/** @type {{ data: import('./$types').PageData }} */
	let { data } = $props();

	// THIS CODE IS BUGGY!
	const wordCount = data.content.split(' ').length;
	const estimatedReadingTime = wordCount / 250;
</script>

<header>
	<h1>{data.title}</h1>
	<p>Reading time: {Math.round(estimatedReadingTime)} minutes</p>
</header>

<div>{@html data.content}</div>
<script lang="ts">
	import type { PageData } from './$types';

	let { data }: { data: PageData } = $props();

	// THIS CODE IS BUGGY!
	const wordCount = data.content.split(' ').length;
	const estimatedReadingTime = wordCount / 250;
</script>

<header>
	<h1>{data.title}</h1>
	<p>Reading time: {Math.round(estimatedReadingTime)} minutes</p>
</header>

<div>{@html data.content}</div>

…その後、/blog/my-short-postから/blog/my-long-postに移動しても、レイアウト、ページ、およびその内部の他のコンポーネントは破棄および再作成されません。代わりに、dataプロパティ(および拡張してdata.titledata.content)が更新され(他のSvelteコンポーネントと同様に)、コードが再実行されないため、onMountonDestroyなどのライフサイクルメソッドは再実行されず、estimatedReadingTimeは再計算されません。

代わりに、値をリアクティブにする必要があります。

src/routes/blog/[slug]/+page
<script>
	/** @type {{ data: import('./$types').PageData }} */
	let { data } = $props();

	let wordCount = $state(data.content.split(' ').length);
	let estimatedReadingTime = $derived(wordCount / 250);
</script>
<script lang="ts">
	import type { PageData } from './$types';

	let { data }: { data: PageData } = $props();

	let wordCount = $state(data.content.split(' ').length);
	let estimatedReadingTime = $derived(wordCount / 250);
</script>

ナビゲーション後にonMountonDestroyのコードを再度実行する必要がある場合は、それぞれafterNavigatebeforeNavigateを使用できます。

このようにコンポーネントを再利用すると、サイドバーのスクロール状態などが保持され、変化する値間を簡単にアニメーション化できます。ナビゲーション時にコンポーネントを完全に破棄して再マウントする必要がある場合は、このパターンを使用できます。

{#key $page.url.pathname}
	<BlogPost title={data.title} content={data.title} />
{/key}

URLへの状態の保存

リロードを乗り越える必要があり、またはSSRに影響を与える状態(たとえば、テーブルのフィルターまたはソートルール)がある場合は、URL検索パラメーター(?sort=price&order=ascendingなど)に配置するのが適切です。それらは<a href="...">または<form action="...">属性に配置するか、goto('?key=value')を介してプログラムで設定できます。それらは、load関数内ではurlパラメーターを介して、コンポーネント内では$page.url.searchParamsを介してアクセスできます。

スナップショットへの一時的な状態の保存

「アコーディオンは開いていますか?」などのUI状態は使い捨てです—ユーザーが離れたり、ページを更新したりした場合、状態が失われても問題ありません。場合によっては、ユーザーが別のページに移動して戻ってきた場合にデータを保持する必要がありますが、URLまたはデータベースに状態を保存するのは過剰です。そのため、SvelteKitはスナップショットを提供します。これにより、コンポーネントの状態を履歴エントリに関連付けることができます。

GitHubでこのページを編集する