状態管理
クライアントのみのアプリケーションの構築に慣れている場合、サーバーとクライアントにまたがるアプリケーションの状態管理は、最初は難しいように見えるかもしれません。このセクションでは、いくつかのよくある問題を回避するためのヒントを示します。
サーバーでの共有状態を避ける
ブラウザは状態を持つ—状態は、ユーザーがアプリケーションと対話する際にメモリに保存されます。一方、サーバーは状態を持たない—レスポンスの内容は、リクエストの内容によって完全に決定されます。
概念的にはそうです。実際には、サーバーはしばしば長期間稼働し、複数のユーザーによって共有されます。そのため、共有変数にデータを保存しないことが重要です。たとえば、次のコードを考えてみましょう。
let let user: any
user;
/** @type {import('./$types').PageServerLoad} */
export function function load(): {
user: any;
}
load() {
return { user: any
user };
}
/** @satisfies {import('./$types').Actions} */
export const const actions: {
default: ({ request }: {
request: any;
}) => Promise<void>;
}
actions = {
default: ({ request }: {
request: any;
}) => Promise<void>
default: async ({ request: any
request }) => {
const const data: any
data = await request: any
request.formData();
// NEVER DO THIS!
let user: any
user = {
name: any
name: const data: any
data.get('name'),
embarrassingSecret: any
embarrassingSecret: const data: any
data.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: any
user;
export const const load: PageServerLoad
load: 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: any
user };
};
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: FormData
data = await request: Request
The original request object
request.Body.formData(): Promise<FormData>
formData();
// NEVER DO THIS!
let user: any
user = {
name: FormDataEntryValue | null
name: const data: FormData
data.FormData.get(name: string): FormDataEntryValue | null
get('name'),
embarrassingSecret: FormDataEntryValue | null
embarrassingSecret: const data: FormData
data.FormData.get(name: string): FormDataEntryValue | null
get('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
関数内でストアに書き込もうとする場合があります。
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>>
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: Response
response = 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) => void
set(await const response: Response
response.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: PageLoad
load: 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: Response
response = 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) => void
set(await const response: Response
response.Body.json(): Promise<any>
json());
};
前の例と同様に、これはあるユーザーの情報がすべてのユーザーによって共有される場所に配置されます。代わりに、データを返すだけです…
/** @type {import('./$types').PageServerLoad} */
export async function function load({ fetch }: {
fetch: any;
}): Promise<{
user: any;
}>
load({ fetch: any
fetch }) {
const const response: any
response = await fetch: any
fetch('/api/user');
return {
user: any
user: await const response: any
response.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: PageServerLoad
load: 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: Response
response = await fetch: (input: string | URL | globalThis.Request, init?: RequestInit) => Promise<Response> (+1 overload)
fetch('/api/user');
return {
user: any
user: await const response: Response
response.Body.json(): Promise<any>
json()
};
};
…そして、それを必要とするコンポーネントに渡すか、$page.data
を使用します。
SSRを使用していない場合、誤ってあるユーザーのデータを別のユーザーに公開するリスクはありません。しかし、load
関数での副作用は依然として避けるべきです—アプリケーションはそれらがない方がはるかに簡単に理解できます。
コンテキストでのストアの使用
独自のストアを使用できない場合、どのように$page.data
や他のアプリストアを使用できるのか疑問に思うかもしれません。答えは、サーバー上のアプリストアがSvelteのコンテキストAPIを使用していることです—ストアはsetContext
を使用してコンポーネントツリーにアタッチされ、サブスクライブするとgetContext
を使用して取得します。独自のストアでも同じことができます。
<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>
<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は既存のレイアウトとページコンポーネントを再利用します。たとえば、次のようなルートがあるとします…
<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.title
とdata.content
)が更新され(他のSvelteコンポーネントと同様に)、コードが再実行されないため、onMount
やonDestroy
などのライフサイクルメソッドは再実行されず、estimatedReadingTime
は再計算されません。
代わりに、値をリアクティブにする必要があります。
<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>
ナビゲーション後に
onMount
とonDestroy
のコードを再度実行する必要がある場合は、それぞれafterNavigateとbeforeNavigateを使用できます。
このようにコンポーネントを再利用すると、サイドバーのスクロール状態などが保持され、変化する値間を簡単にアニメーション化できます。ナビゲーション時にコンポーネントを完全に破棄して再マウントする必要がある場合は、このパターンを使用できます。
{#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はスナップショットを提供します。これにより、コンポーネントの状態を履歴エントリに関連付けることができます。