Tauriの基礎 - ストア
概要
TauriのStoreプラグインは、アプリケーションの設定やデータを永続的に保存するためのキーバリューストアを提供する。
このプラグインは、Webブラウザの localStorage や sessionStorage とは異なり、ファイルシステム上のJSONファイルにデータを保存するため、アプリケーションを再起動してもデータが保持される。
Storeプラグインの主な特徴は以下の通りである。
- 型安全なデータアクセス
- TypeScriptのジェネリクスを使用して、保存・取得するデータの型を指定できる。
- 自動保存と手動保存の選択
- デフォルトで変更を自動的にファイルに保存するが、手動保存に切り替えることも可能である。
- 変更の監視
onKeyChangeメソッドを使用して、特定のキーの値が変更された時にコールバックを実行できる。
- 複数ストアのサポート
- 用途に応じて複数のストアファイルを作成・管理できる。
Tauri v2では、StoreプラグインはRust crate (tauri-plugin-store) と npmパッケージ (@tauri-apps/plugin-store) の2層構造で提供される。
フロントエンドからはTypeScript / JavaScript APIを使用してデータの読み書きを行う。
主な使用場面は以下の通りである。
- アプリケーション設定の永続化
- テーマ、言語、ウインドウサイズなどのユーザ設定を保存する。
- ユーザデータのキャッシュ
- APIから取得したデータを一時的に保存し、オフライン時にもアクセス可能にする。
- セッション状態の管理
- ログイン状態や最後に開いたファイルなどの情報を保持する。
インストールと設定
プラグインのインストール
Storeプラグインをプロジェクトに追加するには、以下に示すコマンドを実行する。
# Tauri CLIを使用してプラグインを追加 npm run tauri add store
このコマンドは以下の処理を自動的に実行する。
- Rustクレートの追加
- src-tauri/Cargo.toml ファイルに tauri-plugin-store が追加される。
- npmパッケージのインストール
- @tauri-apps/plugin-store がインストールされる。
- 基本的な権限設定の生成
- Capabilitiesファイルに必要な権限が追加される。
手動でインストールする場合は、以下に示す手順を実行する。
# npmパッケージのインストール npm install @tauri-apps/plugin-store # Cargo.tomlファイルへ以下に示す設定が自動的に追加される # [dependencies] # tauri-plugin-store = "2"
Rust側の設定
src-tauri/src/lib.rs ファイル または src-tauri/src/main.rs ファイルでプラグインを登録する。
// src-tauri/src/lib.rs
fn main() {
tauri::Builder::default()
.plugin(tauri_plugin_store::Builder::default().build())
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
Capabilities設定
Storeプラグインを使用するには、Capabilitiesファイルで権限を設定する必要がある。
- ファイルの場所
- src-tauri/capabilities/default.json
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
"store:default",
"store:allow-get",
"store:allow-set",
"store:allow-save",
"store:allow-load",
"store:allow-clear",
"store:allow-delete",
"store:allow-entries",
"store:allow-has",
"store:allow-keys",
"store:allow-values",
"store:allow-length"
]
}
store:default 権限を指定するだけで基本的な操作が可能である。
より詳細な制御が必要な場合は、個別の権限を指定することもできる。
Storeインスタンスの作成
基本的な作成方法
load 関数を使用してストアインスタンスを作成する。
// lib/store.ts
import { load } from '@tauri-apps/plugin-store';
// デフォルト設定でストアを作成
// ファイルが存在しない場合は新規作成される
const store = await load('settings.json');
オプションの指定
ストア作成時にオプションを指定できる。
import { load } from '@tauri-apps/plugin-store';
interface StoreOptions {
// 自動保存の設定
// true : 自動保存を有効 (デフォルト: 100[ms]のデバウンス)
// false : 手動保存のみ
// number : 指定したミリ秒でデバウンスして自動保存
autoSave?: boolean | number;
}
// 自動保存を無効化
const manualStore = await load('settings.json', { autoSave: false });
// カスタムデバウンスタイム (500[ms])
const debouncedStore = await load('settings.json', { autoSave: 500 });
// デフォルト設定 (100[ms]デバウンス)
const defaultStore = await load('settings.json', { autoSave: true });
複数ストアの管理
用途に応じて複数のストアファイルを使用できる。
// lib/stores.ts
import { load, Store } from '@tauri-apps/plugin-store';
// アプリケーション設定用ストア
export const settingsStore = await load('settings.json');
// ユーザデータ用ストア
export const userDataStore = await load('user-data.json');
// キャッシュ用ストア (手動保存)
export const cacheStore = await load('cache.json', { autoSave: false });
// ストアのパスはアプリケーションのデータディレクトリ内に保存される
// 例 : ~/.config/myapp/settings.json (Linux)
// 例 : %APPDATA%/myapp/settings.json (Windows)
TypeScript型定義
ストアで扱うデータの型を定義することにより、型安全なアクセスが可能になる。
// types/settings.ts
export interface AppSettings {
theme: 'light' | 'dark' | 'system';
language: string;
fontSize: number;
sidebar: {
collapsed: boolean;
width: number;
};
notifications: {
enabled: boolean;
sound: boolean;
};
}
export interface UserData {
lastOpenedFile: string | null;
recentFiles: string[];
bookmarks: string[];
}
// lib/typedStore.ts
import { load, Store } from '@tauri-apps/plugin-store';
import type { AppSettings, UserData } from '../types/settings';
// 型付きストアクラス
export class TypedStore<T extends Record<string, unknown>> {
private store: Store;
private path: string;
constructor(store: Store, path: string) {
this.store = store;
this.path = path;
}
// 型安全なget
get<K extends keyof T>(key: K): Promise<T[K] | undefined> {
return this.store.get(key as string) as Promise<T[K] | undefined>;
}
// 型安全なset
set<K extends keyof T>(key: K, value: T[K]): Promise<void> {
return this.store.set(key as string, value);
}
// 型安全なhas
has<K extends keyof T>(key: K): Promise<boolean> {
return this.store.has(key as string);
}
// 型安全なdelete
delete<K extends keyof T>(key: K): Promise<boolean> {
return this.store.delete(key as string);
}
}
// 使用例
export const typedSettingsStore = new TypedStore<AppSettings>(
await load('settings.json'),
'settings.json'
);
データの読み書き
基本的なget / set操作
get と set メソッドでデータの読み書きを行う。
// lib/settingsManager.ts
import { load } from '@tauri-apps/plugin-store';
const store = await load('settings.json');
// データの保存 (set)
await store.set('theme', 'dark');
await store.set('language', 'ja');
await store.set('fontSize', 14);
// データの取得 (get)
const theme = await store.get<string>('theme');
console.log(theme); // 'dark'
// 存在しないキーの場合は undefined を返す
const unknown = await store.get<string>('unknown');
console.log(unknown); // undefined
// デフォルト値を設定したい場合
const fontSize = await store.get<number>('fontSize') ?? 12;
複雑なオブジェクトの保存
オブジェクトや配列も保存できる。
// types/user.ts
interface UserProfile {
id: string;
name: string;
email: string;
preferences: {
newsletter: boolean;
notifications: boolean;
};
}
// lib/userStore.ts
import { load } from '@tauri-apps/plugin-store';
import type { UserProfile } from '../types/user';
const store = await load('user.json');
// オブジェクトの保存
const userProfile: UserProfile = {
id: 'user-123',
name: 'John Doe',
email: 'john@example.com',
preferences: {
newsletter: true,
notifications: false,
},
};
await store.set('profile', userProfile);
// オブジェクトの取得
const profile = await store.get<UserProfile>('profile');
console.log(profile?.name); // 'John Doe'
// 配列の保存
await store.set('recentFiles', [
'/path/to/file1.txt',
'/path/to/file2.txt',
'/path/to/file3.txt',
]);
// 配列の取得
const recentFiles = await store.get<string[]>('recentFiles') ?? [];
値の更新と削除
データの更新と削除の方法を示す。
// lib/storeOperations.ts
import { load } from '@tauri-apps/plugin-store';
const store = await load('data.json');
// 値の更新 (上書き)
await store.set('counter', 0);
await store.set('counter', 1); // 0 -> 1
// オブジェクトの部分更新
interface Settings {
theme: string;
language: string;
}
const currentSettings = await store.get<Settings>('settings') ?? { theme: 'light', language: 'en' };
await store.set('settings', {
...currentSettings,
theme: 'dark', // テーマのみ更新
});
// 値の削除 (delete)
await store.set('temp', 'temporary data');
await store.delete('temp');
// 削除の確認
const deleted = await store.delete('non-existent-key'); // false
// 全データの削除 (clear)
await store.clear();
// キーの存在確認 (has)
await store.set('existing', 'value');
const hasExisting = await store.has('existing'); // true
const hasNonExisting = await store.has('non-existing'); // false
型安全なアクセスパターン
型定義を使用した安全なデータアクセスの定義例を示す。
// lib/safeStore.ts
import { load, Store } from '@tauri-apps/plugin-store';
// ストアのスキーマ定義
interface StoreSchema {
'app.theme': 'light' | 'dark' | 'system';
'app.language': string;
'app.fontSize': number;
'user.name': string;
'user.email': string;
'data.recentFiles': string[];
}
// 型安全なストアラッパー
export class SafeStore<T extends Record<string, unknown>> {
private store: Store;
constructor(store: Store) {
this.store = store;
}
async get<K extends keyof T>(key: K): Promise<T[K] | undefined> {
return this.store.get(key as string) as Promise<T[K] | undefined>;
}
async getOrDefault<K extends keyof T>(key: K, defaultValue: T[K]): Promise<T[K]> {
const value = await this.get(key);
return value !== undefined ? value : defaultValue;
}
async set<K extends keyof T>(key: K, value: T[K]): Promise<void> {
await this.store.set(key as string, value);
}
async update<K extends keyof T>(key: K, updater: (current: T[K] | undefined) => T[K]): Promise<void> {
const current = await this.get(key);
await this.set(key, updater(current));
}
}
// 使用例
const safeStore = new SafeStore<StoreSchema>(await load('app.json'));
// デフォルト値付きで取得
const theme = await safeStore.getOrDefault('app.theme', 'system');
// 関数で更新
await safeStore.update('app.fontSize', (current) => (current ?? 12) + 1);
// 配列の更新
await safeStore.update('data.recentFiles', (files) => {
const current = files ?? [];
return [newFilePath, ...current.filter(f => f !== newFilePath)].slice(0, 10);
});
変更監視
onKeyChangeの使用
特定のキーの値が変更された時にコールバックを実行できる。
// lib/storeWatcher.ts
import { load } from '@tauri-apps/plugin-store';
const store = await load('settings.json');
// キーの変更を監視
const unlisten = await store.onKeyChange('theme', (newValue, oldValue) => {
console.log(`Theme changed: ${oldValue} -> ${newValue}`);
// テーマ変更時の処理
applyTheme(newValue);
});
// 値を変更するとコールバックが実行される
await store.set('theme', 'dark'); // コンソールにログが出力される
// 監視を停止する場合
unlisten();
特定キーの監視
複数のキーを監視する定義例を示す。
// hooks/useStoreWatcher.ts
// Store内の特定キーの変更を監視するカスタムフック
import { load, Store } from '@tauri-apps/plugin-store';
import { useEffect, useRef } from 'react';
// イベントリスナー解除関数の型
type UnlistenFn = () => void;
/**
* Store内の特定キーの変更を監視するフック
* @param store - 監視対象のStoreインスタンス
* @param key - 監視するキー名
* @param callback - 値変更時に呼ばれるコールバック関数
*/
export function useStoreWatcher<T>(
store: Store,
key: string,
callback: (newValue: T | undefined, oldValue: T | undefined) => void
) {
// コールバック関数をrefで保持 (最新の関数を参照するため)
const callbackRef = useRef(callback);
callbackRef.current = callback;
useEffect(() => {
let unlisten: UnlistenFn | null = null;
// ウォッチャーをセットアップ
const setupWatcher = async () => {
// 指定キーの変更を監視
unlisten = await store.onKeyChange<T>(key, (newValue, oldValue) => {
callbackRef.current(newValue, oldValue);
});
};
setupWatcher();
// クリーンアップ : コンポーネントアンマウント時に監視を解除
return () => {
if (unlisten) {
unlisten();
}
};
}, [store, key]);
}
// 使用例
function ThemeToggle() {
// 現在のテーマ状態
const [theme, setTheme] = useState<string>('light');
// Storeインスタンスへの参照
const storeRef = useRef<Store | null>(null);
// コンポーネントマウント時にStoreを読み込み
useEffect(() => {
load('settings.json').then(store => {
storeRef.current = store;
// 現在のテーマ値を取得して状態にセット
store.get<string>('theme').then(setTheme);
});
}, []);
// テーマの変更を監視
// Store内の'theme'キーが変更されるとコールバックが実行される
useStoreWatcher<string>(storeRef.current!, 'theme', (newTheme) => {
if (newTheme) {
setTheme(newTheme); // ローカル状態を更新
// DOM要素にテーマ属性を設定 (CSSでテーマ切り替え用)
document.documentElement.setAttribute('data-theme', newTheme);
}
});
// テーマをトグルする関数
const toggleTheme = async () => {
const newTheme = theme === 'light' ? 'dark' : 'light';
// Storeに新しいテーマ値を保存 (監視中のコールバックが自動的に呼ばれる)
await storeRef.current?.set('theme', newTheme);
};
return (
<button onClick={toggleTheme}>
Current theme: {theme}
</button>
);
}
Reactでの活用 (useState連携)
ストアの値をReactの状態と同期させるカスタムフックの定義例を示す。
// hooks/useStoreState.ts
// ストアの値をReact状態として使用するカスタムフック
import { load, Store } from '@tauri-apps/plugin-store';
import { useState, useEffect, useCallback } from 'react';
/**
* ストアの値をReact状態として使用するフック
* @param storePath - ストアファイルのパス
* @param key - ストア内のキー
* @param defaultValue - デフォルト値
* @returns [値, 更新関数, ロード中かどうか, エラー]
*/
export function useStoreState<T>(
storePath: string,
key: string,
defaultValue: T
): [T, (value: T | ((prev: T) => T)) => Promise<void>, boolean, Error | null] {
const [value, setValue] = useState<T>(defaultValue); // 現在の値を保持する状態
const [loading, setLoading] = useState(true); // ロード中かどうかのフラグ
const [error, setError] = useState<Error | null>(null); // エラー情報
const [store, setStore] = useState<Store | null>(null); // ストアインスタンス
// ストアの初期化と値の読み込み
useEffect(() => {
let mounted = true; // マウント状態を追跡 (クリーンアップ用)
const initStore = async () => {
try {
// ストアをロード
const s = await load(storePath);
if (!mounted) return; // アンマウント済みなら中断
setStore(s);
// ストアから値を取得
const storedValue = await s.get<T>(key);
if (storedValue !== undefined) {
setValue(storedValue);
}
// 値の変更を監視
const unlisten = await s.onKeyChange<T>(key, (newValue) => {
if (mounted && newValue !== undefined) {
setValue(newValue); // 変更があれば状態を更新
}
});
setLoading(false);
return unlisten; // クリーンアップ関数を返す
}
catch (e) {
if (mounted) {
setError(e instanceof Error ? e : new Error('Unknown error'));
setLoading(false);
}
}
};
const cleanup = initStore();
// クリーンアップ処理
return () => {
mounted = false;
cleanup.then(fn => fn?.()); // 監視を解除
};
}, [storePath, key]);
/**
* 値を更新してストアに保存する関数
* @param newValue - 新しい値 または 更新関数
*/
const setStoredValue = useCallback(async (newValue: T | ((prev: T) => T)) => {
if (!store) return;
// 関数形式で渡された場合は前の値から計算
const valueToStore = newValue instanceof Function ? newValue(value) : newValue;
setValue(valueToStore); // ローカル状態を更新
await store.set(key, valueToStore); // ストアに保存
}, [store, key, value]);
return [value, setStoredValue, loading, error];
}
// 使用例
function UserPreferences() {
// テーマ設定を管理
const [theme, setTheme, loading, error] = useStoreState<'light' | 'dark'>(
'settings.json',
'theme',
'light'
);
// フォントサイズを管理
const [fontSize, setFontSize] = useStoreState<number>(
'settings.json',
'fontSize',
14
);
// ロード中の表示
if (loading) return <div>Loading...</div>;
// エラー時の表示
if (error) return <div>Error: {error.message}</div>;
return (
<div>
{/* テーマ選択セレクトボックス */}
<select
value={theme}
onChange={(e) => setTheme(e.target.value as 'light' | 'dark')}
>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
{/* フォントサイズ入力フィールド */}
<input
type="number"
value={fontSize}
onChange={(e) => setFontSize(Number(e.target.value))}
/>
</div>
);
}
永続化の仕組み
ファイルへの保存タイミング
ストアのデータは、JSONファイルとして保存される。
- 自動保存 (デフォルト)
- 値の変更後、100[ms]のデバウンス期間を経て、自動的にファイルに保存される。
- 手動保存
autoSave: falseを指定した場合、save()メソッドを呼び出したタイミングで保存される。
import { load } from '@tauri-apps/plugin-store';
// 自動保存 (デフォルト)
const autoStore = await load('auto.json');
await autoStore.set('key', 'value'); // 100ms後に自動保存
// 手動保存
const manualStore = await load('manual.json', { autoSave: false });
await manualStore.set('key', 'value');
await manualStore.save(); // 明示的に保存を呼び出す
手動保存と自動保存
用途に応じて使い分ける。
// lib/storeStrategies.ts
import { load } from '@tauri-apps/plugin-store';
// パターン1 : 設定値の保存 (自動保存)
export const settingsStore = await load('settings.json', { autoSave: true });
// ユーザが設定を変更するたびに自動保存
// パターン2 : バッチ処理 (手動保存)
export const batchStore = await load('batch.json', { autoSave: false });
async function processBatchItems(items: string[]) {
for (const item of items) {
const count = await batchStore.get<number>(item) ?? 0;
await batchStore.set(item, count + 1);
}
// 全ての処理が終わってから一度だけ保存
await batchStore.save();
}
// パターン3 : カスタムデバウンス (500[ms])
export const debouncedStore = await load('debounced.json', { autoSave: 500 });
// 連続する更新を500[ms]間隔でまとめて保存
保存場所の確認
ストアファイルはアプリケーションのデータディレクトリに保存される。
| プラットフォーム | パス | 例 |
|---|---|---|
| Windows | %APPDATA%/{appName} | C:\Users\<ユーザ名>\AppData\Roaming\MyApp\settings.json |
| MacOS | ~/Library/Application Support/{appName} | /Users/<ユーザ名>/Library/Application Support/MyApp/settings.json |
| Linux | ~/.config/{appName} | /home/<ユーザ名>/.config/MyApp/settings.json |
{appName} は、tauri.conf.json ファイルの productName または identifier で決定される。
サンプルコード
設定管理クラスの定義
アプリケーション全体の設定を管理するクラスの定義例を示す。
// lib/SettingsManager.ts
import { load, Store } from '@tauri-apps/plugin-store';
// アプリケーション全体の設定データの型定義
export interface AppSettings {
// 外観に関連する設定
appearance: {
theme: 'light' | 'dark' | 'system';
fontSize: number;
fontFamily: string;
};
// エディタの動作に関する設定
editor: {
tabSize: number;
insertSpaces: boolean;
wordWrap: boolean;
lineNumbers: boolean;
};
// 一般的なアプリ設定
general: {
language: string;
autoUpdate: boolean;
startMinimized: boolean;
};
}
// 初期値 (デフォルト設定)
const DEFAULT_SETTINGS: AppSettings = {
appearance: {
theme: 'system',
fontSize: 14,
fontFamily: 'Consolas, Monaco, monospace',
},
editor: {
tabSize: 2,
insertSpaces: true,
wordWrap: false,
lineNumbers: true,
},
general: {
language: 'ja',
autoUpdate: true,
startMinimized: false,
},
};
// 設定管理クラス
export class SettingsManager {
private store: Store | null = null; // Tauriプラグインのストアインスタンス (ファイル操作用)
private settings: AppSettings = DEFAULT_SETTINGS; // メモリ上に保持する現在の設定状態
private listeners: Set<(settings: AppSettings) => void> = new Set(); // 設定変更を通知するためのリスナー関数の集合
// 初期化処理 : 設定ファイルの読み込みと監視の開始
async initialize(): Promise<void> {
// settings.jsonファイルを読み込み、自動保存を有効化
this.store = await load('settings.json', { autoSave: true });
// 保存されている設定値を取得
const storedSettings = await this.store.get<AppSettings>('settings');
if (storedSettings) {
// デフォルト値とマージ (アプリ更新などで新しい設定項目が追加された場合にデフォルト値を補完するため)
this.settings = this.mergeDeep(DEFAULT_SETTINGS, storedSettings);
}
// ファイルの変更を監視(外部でファイルが書き換わった場合などに反映)
await this.store.onKeyChange<AppSettings>('settings', (newSettings) => {
if (newSettings) {
this.settings = newSettings;
this.notifyListeners();
}
});
}
// 現在の設定を取得 (コピーを返して外部からの不正な変更を防ぐ)
get(): AppSettings {
return { ...this.settings };
}
// 設定を部分的に更新して保存
async update(updates: Partial<AppSettings>): Promise<void> {
this.settings = this.mergeDeep(this.settings, updates);
await this.store?.set('settings', this.settings);
}
// 設定をデフォルト値にリセット
async reset(): Promise<void> {
this.settings = DEFAULT_SETTINGS;
await this.store?.set('settings', this.settings);
}
// 設定変更時のコールバック (リスナー) を登録
// 戻り値としてリスナー解除用の関数を返す
subscribe(listener: (settings: AppSettings) => void): () => void {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
}
// 登録されているすべてのリスナーに変更を通知
private notifyListeners(): void {
this.listeners.forEach(listener => listener(this.settings));
}
// オブジェクトを再帰的にマージするヘルパー関数
// ネストされた設定オブジェクトも深くマージするために使用
private mergeDeep<T>(target: T, source: Partial<T>): T {
const result = { ...target };
for (const key in source) {
if (source[key] !== undefined) {
if (
typeof source[key] === 'object' &&
source[key] !== null &&
!Array.isArray(source[key])
) {
// ネストされたオブジェクトの場合は再帰的に処理
result[key] = this.mergeDeep(
(target as Record<string, unknown>)[key] as Record<string, unknown>,
source[key] as Record<string, unknown>
) as T[Extract<keyof T, string>];
}
else {
// プリミティブな値はそのまま上書き
result[key] = source[key] as T[Extract<keyof T, string>];
}
}
}
return result;
}
}
// アプリケーション全体で共有するシングルトンインスタンス
export const settingsManager = new SettingsManager();
テーマ設定の例
テーマを永続化し、アプリケーション起動時に復元する例を示す。
// hooks/useTheme.ts
import { useState, useEffect, useCallback } from 'react';
import { load } from '@tauri-apps/plugin-store';
// テーマの種類を定義
type Theme = 'light' | 'dark' | 'system';
export function useTheme() {
const [theme, setThemeState] = useState<Theme>('system'); // ユーザが選択したテーマ設定 ('system'含む)
const [resolvedTheme, setResolvedTheme] = useState<'light' | 'dark'>('light'); // 実際に適用されるテーマ ('light' または 'dark'のみ)
const [loading, setLoading] = useState(true); // 初期化中かどうかのフラグ
// OSのシステム設定から現在のテーマを検出する関数
const getSystemTheme = (): 'light' | 'dark' => {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
};
// テーマをDOMと状態に反映させる関数
const applyTheme = useCallback((newTheme: Theme) => {
const resolved = newTheme === 'system' ? getSystemTheme() : newTheme; // 'system'の場合はOSの設定を確認して、実効テーマを決定
setResolvedTheme(resolved);
document.documentElement.setAttribute('data-theme', resolved); // HTMLタグのdata属性を変更してCSS等に適用
}, []);
// コンポーネントマウント時の初期化処理
useEffect(() => {
const initTheme = async () => {
// 設定ファイルをロード
const store = await load('settings.json');
// 保存されたテーマを取得 (なければデフォルトである'system'を使用)
const storedTheme = await store.get<Theme>('theme') ?? 'system';
setThemeState(storedTheme);
applyTheme(storedTheme);
setLoading(false);
// 設定ファイルの変更を監視 (他のウインドウ等での変更を同期)
await store.onKeyChange<Theme>('theme', (newTheme) => {
if (newTheme) {
setThemeState(newTheme);
applyTheme(newTheme);
}
});
// OSのシステムテーマ設定の変更を監視
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleChange = () => {
// 現在の設定が'system'の場合のみ、変更を即時反映
if (theme === 'system') {
applyTheme('system');
}
};
mediaQuery.addEventListener('change', handleChange);
// クリーンアップ関数 : リスナーを削除
return () => mediaQuery.removeEventListener('change', handleChange);
};
initTheme();
}, [applyTheme, theme]);
// テーマを変更して保存する関数
const setTheme = useCallback(async (newTheme: Theme) => {
const store = await load('settings.json');
await store.set('theme', newTheme); // ストアに保存すれば、onKeyChangeイベントが発生して画面も更新される
}, []);
return { theme, resolvedTheme, setTheme, loading };
}
// components/ThemeToggle.tsx
import { useTheme } from '../hooks/useTheme';
export function ThemeToggle() {
// テーマ管理用のカスタムフックから状態と関数を取得
const { theme, resolvedTheme, setTheme, loading } = useTheme();
// ロード中は何も表示しない(またはローディングスピナー等を表示)
if (loading) return null;
return (
<select
value={theme} // 現在の設定値を選択状態にする
onChange={(e) => setTheme(e.target.value as Theme)}
className="theme-select"
>
<option value="light">Light</option>
<option value="dark">Dark</option>
{/* System選択時は、現在の実効テーマを括弧内に表示 */}
<option value="system">System ({resolvedTheme})</option>
</select>
);
}
Reactカスタムフック
ストア操作を簡略化する汎用的なカスタムフックの定義例を示す。
// hooks/usePersistentState.ts
import { useState, useEffect, useCallback, useRef } from 'react';
import { load, Store } from '@tauri-apps/plugin-store';
// ストア操作をカプセル化し、永続化機能を持つカスタムフック
// useStateと同じ感覚で使えるが、値はファイルに保存される
export function usePersistentState<T>(
key: string, // ストア内でのキー
defaultValue: T, // 初期値(値がない場合に使用)
storePath: string = 'state.json' // 保存先ファイル名
): [T, (value: T | ((prev: T) => T)) => void, boolean, (value: T) => Promise<void>] {
const [state, setState] = useState<T>(defaultValue); // 現在の状態
const [loading, setLoading] = useState(true); // ローディング状態 (初期化完了までtrue)
const storeRef = useRef<Store | null>(null); // Storeインスタンスを保持 (コンポーネントの再レンダリング間で維持するためRefを使用)
const isInitialized = useRef(false); // 初期化完了フラグ
// ストアの初期化処理
useEffect(() => {
let mounted = true;
const init = async () => {
try {
// ストアをロード (autoSave: true で変更を自動保存)
const store = await load(storePath, { autoSave: true });
if (!mounted) return;
storeRef.current = store;
// 保存されている値を取得して反映
const storedValue = await store.get<T>(key);
if (storedValue !== undefined) {
setState(storedValue);
}
// 他のプロセスやウインドウでの変更を監視 (マルチウインドウ対応等)
const unlisten = await store.onKeyChange<T>(key, (newValue) => {
if (newValue !== undefined) {
setState(newValue);
}
});
setLoading(false);
isInitialized.current = true;
// クリーンアップ関数としてリスナー解除関数を返す
return unlisten;
}
catch (error) {
console.error('Failed to initialize store:', error);
setLoading(false);
}
};
const cleanup = init();
return () => {
mounted = false;
// 非同期のクリーンアップ処理を実行
cleanup.then(fn => fn?.());
};
}, [key, storePath]);
// 状態を更新する関数
// useStateのセッターと同様のインターフェース (値または関数を受け取る)
const setPersistentState = useCallback((value: T | ((prev: T) => T)) => {
setState((prev) => {
const newValue = value instanceof Function ? value(prev) : value; // 関数が渡された場合は実行、値が渡された場合はそのまま使用
// ストアに非同期で保存
if (storeRef.current) {
storeRef.current.set(key, newValue).catch(console.error);
}
return newValue;
});
}, [key]);
// 現在の状態を明示的にストアに保存する関数 (必要に応じて使用)
const forceSave = useCallback(async (value: T) => {
if (storeRef.current) {
await storeRef.current.set(key, value);
}
}, [key]);
// [現在の値, セッター, ローディング状態, 強制保存関数]を返す
return [state, setPersistentState, loading, forceSave];
}
// 使用例
// Todoアイテムの型定義
interface Todo {
id: number;
text: string;
completed: boolean;
}
function TodoList() {
// 永続化されるTodoリストの状態を管理
// キー名 'todos' で state.json に保存される
const [todos, setTodos, loading] = usePersistentState<Todo[]>(
'todos',
[]
);
// Todoの追加処理
const addTodo = (text: string) => {
setTodos(prev => [
...prev,
{ id: Date.now(), text, completed: false }
]);
};
// Todoの完了状態を切り替え
const toggleTodo = (id: number) => {
setTodos(prev =>
prev.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
};
if (loading) return <div>Loading...</div>;
return (
<div>
<button onClick={() => addTodo('New task')}>Add</button>
<ul>
{todos.map(todo => (
<li key={todo.id} onClick={() => toggleTodo(todo.id)}>
{todo.text} - {todo.completed ? 'Done' : 'Pending'}
</li>
))}
</ul>
</div>
);
}
推奨される事柄
データ構造の設計
ストアに保存するデータ構造は、拡張性を考慮して設計する。
// 良い例 : 拡張可能な構造
interface GoodSettings {
version: number; // マイグレーション用
appearance: {
theme: string;
fontSize: number;
// 将来の拡張用
};
editor: {
tabSize: number;
// 将来の拡張用
};
}
// 悪い例 : フラットな構造
interface BadSettings {
theme: string;
fontSize: number;
tabSize: number;
// 新しい設定を追加すると構造が複雑になる
}
マイグレーション戦略
設定のバージョン管理とマイグレーションの定義例を示す。
// lib/settingsMigration.ts
import { load } from '@tauri-apps/plugin-store';
interface SettingsV1 {
theme: string;
fontSize: number;
}
interface SettingsV2 {
version: 2;
appearance: {
theme: string;
fontSize: number;
};
editor: {
tabSize: number;
};
}
const CURRENT_VERSION = 2;
// マイグレーション関数
function migrateV1toV2(v1: SettingsV1): SettingsV2 {
return {
version: 2,
appearance: {
theme: v1.theme,
fontSize: v1.fontSize,
},
editor: {
tabSize: 2,
},
};
}
// マイグレーションを実行
export async function migrateSettings(): Promise<SettingsV2> {
const store = await load('settings.json');
const stored = await store.get<SettingsV1 | SettingsV2>('settings');
if (!stored) {
// 新規インストール
return {
version: CURRENT_VERSION,
appearance: { theme: 'light', fontSize: 14 },
editor: { tabSize: 2 },
};
}
// バージョンチェック
if (!('version' in stored)) {
// V1からV2へマイグレーション
const migrated = migrateV1toV2(stored);
await store.set('settings', migrated);
return migrated;
}
return stored as SettingsV2;
}
パフォーマンスのヒント
ストアのパフォーマンスを最適化するためのヒントを示す。
- 大きなデータは分割する。
- 巨大な配列やオブジェクトは複数のキーに分割して保存する。
- デバウンスを活用する。
- 頻繁な更新がある場合は、
autoSaveのデバウンスタイムを調整する。
- 頻繁な更新がある場合は、
- 不要なデータは削除する。
- 使用しなくなったデータは
deleteで削除する。
- 使用しなくなったデータは
- 初期読み込みを最適化する。
- アプリケーション起動時に必要なデータのみを先に読み込む。
トラブルシューティング
データが保存されない問題
以下に示す項目を確認する。
- Capabilities設定の確認
store:allow-setとstore:allow-save権限が設定されているか確認する。
save()の呼び出し確認- 手動保存モードの場合、
save()が呼び出されているか確認する。
- 手動保存モードの場合、
- エラーハンドリングの実装
setやsaveのエラーをキャッチしているか確認する。
型エラーの解決
TypeScriptの型エラーが発生する場合の対処法を示す。
// エラー : 'undefined' is not assignable to type 'string'
const theme: string = await store.get<string>('theme');
// 解決策1 : undefinedを考慮する
const theme: string | undefined = await store.get<string>('theme');
// 解決策2 : デフォルト値を使用する
const theme: string = await store.get<string>('theme') ?? 'light';
// 解決策3 : 型ガードを使用する
const storedTheme = await store.get<string>('theme');
if (typeof storedTheme === 'string') {
const theme: string = storedTheme;
}
関連情報