【ITニュース解説】Mastering Svelte Custom Stores
2025年09月13日に「Dev.to」が公開したITニュース「Mastering Svelte Custom Stores」について初心者にもわかりやすく解説しています。
ITニュース概要
Svelteのカスタムストアは、単純なデータ共有ストアを拡張し、ビジネスロジックや永続化機能を内蔵できる。これにより、共通する複雑な状態管理や操作ルールをコンポーネントから分離し、ストア内で一元化できるため、アプリの整合性と保守性が向上する。
ITニュース解説
Svelte(スベルト)というWebアプリケーション開発フレームワークでは、コンポーネント間で共通のデータ(状態)を共有するための「ストア」という仕組みがある。これは、アプリケーション内で複数の部品が同じ値を読み書きする必要がある場合に非常に便利だ。たとえば、サイト全体のテーマ(ライトモードかダークモードか)、現在ログインしているユーザーの情報、あるいは複数のページで共有されるカウンターなどがこれにあたる。
基本的なストアは、writableという関数を使って作成する。writable(0)のように初期値を与えると、このストアは数字の0を保持し、その値を読み取ったり、新しい値に書き換えたり、現在の値に基づいて更新したりする機能を持つ。Svelteのコンポーネント内では、このストアの名前の前に$を付けるだけで、自動的にストアの値が購読され、その時点の値をテンプレートに表示できる。ストアの値が変化すれば、表示も自動的に更新されるため、非常に少ない記述でリアクティブなUIを構築できるのがSvelteの強みだ。例えば、{$count}と書けばcountストアの現在の値が表示され、$count += 1と書けばストアの値を直接更新できる。この$ショートカットはSvelteのコンポーネントファイル(.svelteファイル)でのみ利用でき、通常のJavaScriptファイルやTypeScriptファイルでストアの値を読み取る場合は、subscribe()メソッドやget()ヘルパー関数を利用する必要がある。
この基本的なwritableストアは多くのケースで十分役立つが、アプリケーションが複雑になるにつれて、ストアに単に値を保持するだけでなく、特定のルールや振る舞いを組み込みたくなる場面が出てくる。例えば、カウンターが常に特定の最大値を超えないようにしたり、0未満にならないようにしたり、あるいはカウンターが変更された履歴を記録したり、一定時間後に自動でリセットされるようにしたりといった要件だ。もしこれらのルールをストアを利用する各コンポーネントにそれぞれ記述すると、同じロジックが重複し、バグの原因になりやすい。このような場合に、「カスタムストア」が役立つ。
カスタムストアとは、基本的なストアに「スーパーパワー」を与えたものと考えると分かりやすい。単に値を保持するだけでなく、その値に対するビジネスロジック、データの永続化(ページを閉じても値を記憶しておく機能)、あるいは他のシステムとの連携(副作用)などを、ストア自身の中に組み込むことができる。
カスタムストアの作り方は比較的シンプルで、まず基本的なwritableストアを用意する。次に、そのwritableストアを囲む関数を作り、その関数がオブジェクトを返すようにする。この返されるオブジェクトには、subscribeメソッド($ショートカットが機能するために必要)と、そのストア固有の振る舞いを表すカスタムメソッド(例えばincrement、decrement、resetなど)を含める。
例として、最大値が設定できるカウンターを考えてみよう。createCounter(max = 10)という関数を作成し、その中でwritable(0)を生成する。そして、この関数がsubscribeメソッドと、increment、decrement、resetというメソッドを持つオブジェクトを返す。incrementメソッドの中では、現在の値に1を足すが、その結果がmaxを超えないようにMath.min()を使って制限するロジックを記述する。同様に、decrementメソッドではMath.max()を使って値が0未満にならないように制限する。さらに、historyという内部変数を定義し、値が変更されるたびにその履歴を記録するようにすれば、コンポーネントはgetHistory()メソッドを呼び出すだけで履歴を取得できる。このように、全てのルールとロジックをストアの中にカプセル化することで、コンポーネントは単にcounter.increment()やcounter.reset()のような安全なメソッドを呼び出すだけで済むようになり、アプリケーション全体の一貫性が保たれ、保守が容易になる。
データの永続化もカスタムストアの重要な機能の一つだ。例えば、ユーザーが設定したウェブサイトのテーマ(ライトモードかダークモードか)を、ページをリロードしても記憶させたい場合がある。これにはブラウザのlocalStorageという機能が利用できる。しかし、SvelteKitのようなフレームワークでは、アプリケーションのコードがウェブページを初めて表示する際に「サーバー側」(SSR: Server-Side Rendering)でも実行され、その後に「ブラウザ側」でも実行されるという特徴がある。localStorageはブラウザにのみ存在する機能なので、サーバー側でlocalStorageにアクセスしようとするとエラーが発生し、アプリケーションがクラッシュしてしまう。
この問題を避けるため、カスタムストア内でlocalStorageを使用する際には、現在コードがブラウザで実行されているかどうかを$app/environmentからインポートできるbrowserというユーティリティを使って確認する必要がある。例えば、browser ? localStorage.getItem('theme') || 'light' : 'light'のように条件分岐させることで、ブラウザ環境でのみlocalStorageにアクセスし、サーバー環境ではデフォルト値を返すように安全に実装できる。テーマストアの例では、toggleメソッドでテーマを切り替える際にlocalStorageに保存し、Svelteの$effect機能を使って、ストアの値が変更されるたびにウェブページの<body>要素のクラスを更新することで、ページ全体のスタイルを動的に変更する。<body>要素のスタイルはSvelteのコンポーネントのCSSスコープ外になるため、:global()という特別なキーワードを使ってグローバルなCSSルールを適用することも必要になる。
さらに複雑な例として、認証ストアもカスタムストアで実装できる。このストアは、現在ログインしているユーザーの情報を保持し、login()やlogout()といったメソッドを提供する。ログイン時にはユーザー情報をストアに設定し、localStorageにも保存してページをリロードしてもログイン状態が維持されるようにする。ログアウト時にはストアとlocalStorageからユーザー情報をクリアする。この際も、localStorageへのアクセスはtypeof localStorage !== 'undefined'のようなチェックで安全に行う必要がある。コンポーネントは$authの値に応じてログイン/ログアウトの表示を切り替え、auth.login()やauth.logout()を呼び出すだけでよく、認証の具体的な処理(例えば実際のAPIコールなど)はすべてストア内にカプセル化される。これにより、認証ロジックが変更されても、それを利用するコンポーネントに影響が出にくくなる。
カスタムストアを効果的に使うための注意点もある。一つは、ストアの内部状態をストアの公開メソッド以外から直接変更しないことだ。ストアは「ブラックボックス」のように扱い、状態を変更する必要がある場合は、必ずストアが提供するメソッドを介して行う。これにより、状態の整合性が保たれ、デバッグも容易になる。また、先述の通り、localStorageやwindow、documentといったブラウザ固有のAPIをSvelteKitで利用する際は、必ずbrowser変数やtypeof演算子を使ってブラウザ環境での実行を保証する必要がある。最後に、すべての状態管理にカスタムストアを使う必要はない。単に数値や文字列、真偽値を保持するだけの単純な状態であれば、writable()ストアで十分だ。カスタムストアは、検証、永続化、履歴管理、副作用など、追加のロジックが必要な場合の「強力なツール」として活用することが推奨される。これらの点に注意することで、Svelteアプリケーションにおける状態管理をより体系的かつ堅牢に行うことができるだろう。