まくろぐ
更新: / 作成:

何をするか?

Svelte 5 のコンポーネントで使用する状態変数は $state ルーンを使って定義できますが、ページをリロードするとその値はリセットされてしまいます。 この記事では、$state ルーンで定義した状態変数の値を localStoragesessionStorage と同期させる方法を紹介します。 localStorage と同期させた場合は、ブラウザを開き直したときにも以前の状態を復帰できます。 sessionStorage と同期させた場合は、ブラウザのタブを開いている間だけ状態を保持できます。

$state ルーンを使って状態変数を共有する方法は、下記の記事を参照してください。

実装例

/p/tpjvh3q/img-001.png
図: サインイン状態を管理する AuthState クラスの実装と使用例

ここでは、サインイン状態を管理する AuthState クラスを作成し、その userName プロパティ(状態変数)を localStorage に保存する例を示します。 サインイン状態を示す signedIn プロパティも用意していますが、こちらは $derived ルーンで定義されており、userName プロパティの値に応じて自動的に値が決まるようになっています(userName がセットされているときに true になります)。

src/lib/auth-state.svelte.ts
import { onMount } from 'svelte';

const STORAGE_KEY = 'userName';

// 複数のコンポーネントで状態を共有するため、リアクティブな変数をグローバルに定義します
let userName = $state('');
let signedIn = $derived(!!userName);

export class AuthState {
	/** サインインしているユーザー名を参照するためのプロパティ (reactive) */
	get userName(): string {
		return userName;
	}

	/** サインインしているかどうかを示すプロパティ (reactive) */
	get signedIn(): boolean {
		return signedIn;
	}

	constructor() {
		// ページが読み込まれたときに localStorage の値で状態変数を初期化します
		// (このコードはコンポーネントの初期化中に呼び出す必要があります)
		onMount(() => {
			userName = localStorage.getItem(STORAGE_KEY) || '';
		});
	}

	signIn(): void {
		// 実際には正しいユーザー情報やトークンを取得して保存します
		userName = 'Maku';
		localStorage.setItem(STORAGE_KEY, userName);
	}

	signOut(): void {
		userName = '';
		localStorage.removeItem(STORAGE_KEY);
	}
}

AuthState クラスのコンストラクターでは、Svelte のライフサイクルフックである onMount() 関数を使って、ページが読み込まれたときに localStorage から userName の値を取得しています。 これにより、ページをリロードしたときに以前の状態を復元できます。 Svelte の仕組み上、onMount 関数はコンポーネントの初期化中にしか呼び出せないため、new AuthState() というインスタンス化コードはコンポーネント側に記述する必要があります(後述)。

signIn() および signOut() メソッドは、userName 状態変数の値を変更すると同時に localStorage にもその値を保存することで、次回のページ読み込みに備えています。 ここでは、主な状態変数としてユーザー名だけを扱っていますが、実際のアプリケーションでは、サインイン時に取得したアクセストークンなどの情報を保存することが一般的です。

次に、この AuthState クラスを利用するコンポーネントを作成します。 サインイン/アウトのボタンを持つサイトヘッダーが典型的な例です。

src/lib/components/SiteHeader.svelte
<script lang="ts">
	import { AuthState } from '$lib/auth-state.svelte';
	const authState = new AuthState();  // 内部で onMount フックがセットされる
</script>

<header>
	<h1>My Site</h1>
	{#if authState.signedIn}
		<div>Welcome, {authState.userName}!</div>
		<button onclick={() => authState.signOut()}>Sign out</button>
	{:else}
		<button onclick={() => authState.signIn()}>Sign in</button>
	{/if}
</header>

<style>
	* {
		margin: 0;
	}
	header {
		display: flex;
		justify-content: space-between;
		align-items: center;
		padding: 0.5rem 1rem;
		background-color: #f0f0f0;
	}
	button {
		font-size: 1rem;
		padding: 0.5rem 1rem;
		border: none;
		border-radius: 0.25rem;
		background-color: #007bff;
		color: white;
		cursor: pointer;
	}
	button:hover {
		background: #0065d8;
	}
</style>

このコンポーネントを +layout.svelte に組み込むことで、全ページにサイトヘッダーを表示できます。

src/routes/+layout.svelte
<script lang="ts">
  import SiteHeader from '$lib/components/SiteHeader.svelte';
</script>

<SiteHeader />
<slot />

できたー ٩(๑❛ᴗ❛๑)۶ わーぃ

関連記事

更新: / 作成:

$state ルーンのおさらい

Svelte 5 では、$state ルーンを使ってリアクティブな変数を定義することができます。 下記は公式サイトでも最初に出てくる基本的なカウンター変数の実装例です。

src/lib/components/CounterButton.svelte
<script lang="ts">
	let count = $state(0);
</script>

<button onclick={() => count++}>
	clicks: {count}
</button>

上記のように $state ルーンを使って定義した count 変数はリアクティブな変数として扱われ、count の値が変更されると、それに連動して画面上の表示も更新されます(clicks: 0clicks: 1 になる)。 count の値を変更したあとに再描画処理を呼び出す、という手続き的なコードを書く必要はありません。

リアクティブな変数のロジックを切り出す

$state ルーンを使ったロジックは、.svelte コンポーネントから分離して別ファイルに記述することができます。 これは Svelte 5 でルーンが導入された理由のひとつです。 $state ルーンを使う TypeScript コードの拡張子は、単純な .ts ではなく .svelte.ts にする必要があります(ルーンは特殊なビルド処理が必要なため)。

下記は、リアクティブなカウンター変数のロジックを別ファイルに分離した例です。 $state ルーンだけでなく、$effect ルーンを使ってリアクティブ変数の変化に反応させることもできます。 ここでは、count 変数の値が変更されるたびにコンソールにログを出力するようにしています。

src/lib/counter.svelte.ts
export function createCounter() {
	let count = $state(0);

	$effect(() => {
		console.log(`The count is now ${count}`);
	});

	return {
		get count() { return count; },
		increment: () => { count += 1; }
	};
}

この createCounter() 関数は、任意のコンポーネントから以下のように使用します。

src/lib/components/CounterButton.svelte
<script lang="ts">
	import { createCounter } from '$lib/counter.svelte';  // .ts は付けない
	let counter = createCounter();  // リアクティブなプロパティを持つオブジェクトを生成
</script>

<button onclick={() => counter.increment()}>
	clicks: {counter.count}
</button>

counter オブジェクトの increment() メソッドを呼び出すと、内部で保持しているリアクティブなカウンター変数 (count) の値がインクリメントされるため、それを参照している UI も自動的に更新されます。

リアクティブな変数の値を複数コンポーネントで共有する

.svelte.ts ファイル内でグローバルなリアクティブ変数を定義し、それを export することで、複数のコンポーネントでその変数を共有することができます。 前述の例ではリアクティブ変数の「ロジック」を使いまわせるようにしましたが、今回は、リアクティブ変数の「値」を共有します

src/lib/counter.svelte.ts
export const counter = $state({ count: 0 });

複数のコンポーネントでリアクティブ変数の値を共有したいときは、$state() に渡す初期値はオブジェクトの形で渡す必要がある ことに注意してください。

export const counter = $state(0);  // NG

上記のようにプリミティブな値で初期化してしまうと、利用側のコンポーネントでの再代入処理(counter++ など)が、別の変数への代入として扱われてしまうため、リアクティブな振る舞いが失われてしまうようです(複数コンポーネント間で値が共有されない)。

上記のように export した counter 変数は、以下のように複数のコンポーネントから参照します。

src/lib/components/CounterButton1.svelte
<script lang="ts">
	import { counter } from '$lib/counter.svelte';
</script>

<button onclick={() => counter.count++}>
	Button1: {counter.count}
</button>
src/lib/components/CounterButton2.svelte(ほぼ同じ実装)
<script lang="ts">
	import { counter } from '$lib/counter.svelte';
</script>

<button onclick={() => counter.count++}>
	Button2: {counter.count}
</button>
src/routes/+page.svelte
<script lang="ts">
	import CounterButton1 from '$lib/components/CounterButton1.svelte';
	import CounterButton2 from '$lib/components/CounterButton2.svelte';
</script>

<CounterButton1 />
<CounterButton2 />

2 つのコンポーネントから参照している counter 変数は、実際には同一のオブジェクトを指しています。 そのため、そのプロパティであるリアクティブ変数 counter.count の値が変更されると、2 つのコンポーネントの UI が同時に更新されます。

Svelte 4 の頃は、このような処理は Store の仕組み (svelte/store) を使って実装する必要がありましたが、Svelte 5 では $state ルーンを使って、よりシンプルに記述できるようになりました。

(おまけ)クラス内でリアクティブ変数を使う

クラスのプロパティとしてリアクティブ変数を使うこともできます。 下記の例では、Counter クラスの count プロパティをリアクティブにしています。

src/lib/counter.svelte.ts
export class Counter {
	count: number = $state(0);
}

export const counter = new Counter();

さらに、上記モジュールではグローバル変数として作成した counter オブジェクトを export しているため、複数のコンポーネントで counter.count の値を共有できます。

src/lib/components/CounterButton.svelte
<script lang="ts">
	import { counter } from '$lib/counter.svelte';
</script>

<button onclick={() => counter.count++}>
	clicks: {counter.count}
</button>

この仕組みはとても強力で、リアクティブな変数をカプセル化して、クラス内のロジックを構築していくことができます。

src/lib/counter.svelte.ts
export class Counter {
	#count: number = $state(0);  // Private field

	get count(): number {
		return this.#count;
	}

	increment(): void {
		this.#count += 1;
	}
}

export const counter = new Counter();
src/lib/components/CounterButton.svelte
<script lang="ts">
	import { counter } from '$lib/counter.svelte';
</script>

<button onclick={() => counter.increment()}>
	clicks: {counter.count}
</button>

٩(๑❛ᴗ❛๑)۶ 超便利っ

応用例

関連記事

更新: / 作成:

何をするか?

Svelte/SvelteKit を使った Web アプリケーションでは、src/routes 以下のサーバーモジュール (+server.ts) で GETPOST 関数をエクスポートするだけで、API ルートとして動作させることができます(Web API のエンドポイントとして動作します)。

このサーバーモジュール内でグローバル変数を定義すると、その値は サーバーが起動している間だけ保持されます。 ここでは、この振る舞いを確認するためのサンプルコードを示します。

複数のクライアントからのリクエスト間で同じ変数が共有されるため、ユーザー情報などを格納してしまうと、セキュリティ上のリスクが生じる可能性があることに注意してください。

実装例

下記のサーバールート実装 (/api/messages) では、POST リクエストで送られてきたメッセージを最大 5 件までグローバル変数 (messages) に保持しています。 GET リクエストでアクセスすると、保持しているメッセージを JSON 形式で返します。

src/routes/api/messages/+server.ts
import { error, json } from '@sveltejs/kit';

// API route のグローバル変数はサーバーが起動している間のみ保持される
let messages: string[] = [];

/** /api/messages への GET リクエストを処理する */
export async function GET() {
	return json({ messages });
}

/** /api/messages への POST リクエストを処理する */
export async function POST({ request }) {
	const msg = (await request.text()).trim();
	if (!msg) {
		error(400, 'Text is required in the request body');
	}
	// 最新の 5 つのメッセージを保持
	messages = [...messages.slice(-4), msg];
	return json({ messages }, { status: 201 });
}

ページコンポーネントからこの API ルートにアクセスするコードは以下のようになります。 テキストボックスに入力したメッセージを送信すると、API ルート側で保持しているメッセージリストが更新され、その内容が画面上に表示されます。

src/routes/+page.svelte
<script lang="ts">
	import { onMount } from 'svelte';

	/** サーバーから取得したメッセージのリスト */
	let receivedMessages: string[] = $state([]);

	/** ユーザーが入力したメッセージ */
	let userInput = $state('');

	async function fetchMessages() {
		const res = await fetch('/api/messages');
		const jsonData = await res.json();
		receivedMessages = jsonData.messages;
	}

	async function handleSubmit(e: Event) {
		e.preventDefault();
		const newMessage = userInput.trim();
		userInput = '';

		// 入力欄が空の場合は何もしない
		if (newMessage === '') {
			return;
		}

		// API エンドポイントに新しいメッセージを POST で送信
		const res = await fetch('/api/messages', {
			method: 'POST',
			headers: { 'Content-Type': 'application/json' },
			body: newMessage
		});

		// 新しいデータで表示を更新
		const jsonData = await res.json();
		receivedMessages = jsonData.messages;
	}

	// ページが読み込まれたときにメッセージを取得
	onMount(fetchMessages);
</script>

<main>
	<form>
		<input bind:value={userInput} placeholder="メッセージを入力" />
		<button type="submit" onclick={handleSubmit}>送信</button>
	</form>
	<ul>
		{#each receivedMessages.toReversed() as message}
			<li>{message}</li>
		{/each}
	</ul>
</main>

SvelteKit のサーバーを起動して Web ブラウザーでアクセスすると、次のように最大 5 件のメッセージが表示されます。 サーバーが起動している間はこのメッセージリストは保持されており、別のブラウザーからアクセスしても同じメッセージが表示されます。

/p/c28msnk/img-001.gif
図: API ルートでのデータ保持

理解したー ٩(๑❛ᴗ❛๑)۶ わーぃ

よく考えたら messages まわりの処理がスレッドセーフになっていないので、複数のリクエストが同時に来た場合に備えて Mutex などで排他制御を行わなきゃですね。 いずれにしても、このようなコードを実際のプロダクション環境で使うことは稀で、多くの場合は、データベースや KV ストアライブラリなどを利用してデータの永続化を行うことになります。

関連記事

メニュー

まくろぐ
サイトマップまくへのメッセージ