Store プラグインで設定を保存する(キーバリューストア)

Store プラグインの LazyStore / load() と get()・set() で設定を JSON に保存する。自動保存の間隔、既定値、ウィンドウ間や Rust との共有、壊れたファイルが黙って初期化される落とし穴も示す。

データ保存 対象: Tauri 2.x 更新日: 読了目安: 約8分 db-004
目次
  1. 前提条件
  2. 1. フロントエンドから実装する (TypeScript)
  3. 開き方と既定値を 1 か所で決める
  4. 変更を受け取る・すぐに書き込む
  5. 2. バックエンドから実装する (Rust)
  6. 動作確認
  7. よくあるエラーと対処法
  8. OS ごとの違いと注意点
  9. 関連レシピ

テーマや文字の大きさ、最近開いたファイルの一覧のような小さな設定は、Store プラグインで「キーと値」の組として保存するのが手軽です。中身は JSON ファイルで、読み書きはメモリ上で行い、変更は少し待ってからまとめてファイルに書かれます。同じファイルを開いたウィンドウ同士と Rust 側で同じ内容を共有でき、変更の通知も受け取れます。保存のタイミングと既定値の扱い、ファイルが壊れたときの挙動に注意が要ります。

前提条件

npm run tauri add store

src-tauri/capabilities/default.json に store:default を足します。読み書き・保存・再読み込みのすべての操作が含まれます。変更の通知(onKeyChange())はイベントの購読を使いますが、これは core:default に含まれます。Store の権限にはパスの範囲指定が無く、どのパスのファイルでも開けます。

{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "default",
  "description": "Capability for the main window",
  "windows": ["main"],
  "permissions": [
    "core:default",
    "store:default"
  ]
}

保存先の選び方は次のとおりです。

保存したいもの使うもの
小さな設定・状態(平文でよい)Store プラグイン(このレシピ)
ファイルの形式や書き込み方を自分で決めたい設定自前の JSON ファイル
パスワードや API トークンStronghold
消えても作り直せる UI の状態LocalStorage / IndexedDB
件数が多い・検索するデータSQLite
Excel などとやり取りする表CSV ファイル
ウィンドウの位置と大きさWindow State プラグイン

1. フロントエンドから実装する (TypeScript)

開き方と既定値を 1 か所で決める

LazyStore は最初に使われたときにファイルを読みます(すぐ読むなら await load())。相対パスはアプリのデータフォルダー(appDataDir())が基準です。defaults はファイルの中身と合成され、ファイルに無いキーだけ既定値になるので、新しい版で設定項目を増やしても古いファイルのまま動きます。

注意したいのは、同じパスのストアはアプリ全体で 1 つだけ作られ、後から渡したオプションは無視されることです。ウィンドウごとに違う defaults を書くと、先に開いた側の設定になります。オプションは 1 つのモジュールにまとめ、どのウィンドウからもそれを import します。

import { LazyStore } from '@tauri-apps/plugin-store';

export type Theme = 'system' | 'light' | 'dark';

export const settings = new LazyStore('settings.json', {
  defaults: { theme: 'system', fontSize: 14, recentFiles: [] },
  autoSave: 500, // 最後の変更から 500ms 後にまとめて保存(省略時は 100ms、false で自動保存しない)
});

export async function getTheme(): Promise<Theme> {
  return (await settings.get<Theme>('theme')) ?? 'system'; // 無いキーは undefined
}

export async function setTheme(theme: Theme): Promise<void> {
  await settings.set('theme', theme);
}

get<T>() の型は宣言だけで、中身は確かめません。利用者がファイルを手で書き換えることもあるので、大事な値は読んだ後に確かめます。値は JSON として送られるので、Date は文字列になって戻ります。

変更を受け取る・すぐに書き込む

onKeyChange() は、ほかのウィンドウや Rust で値が変わったときにも呼ばれます。テーマを全ウィンドウでそろえるのに使えます。自動保存は変更から少し遅れて書き込み、アプリが通常の手順で終了するときにも保存されますが、強制終了では直前の変更が失われることがあります。消えると困る変更は save() ですぐ書き込みます。

// (続き)
function applyTheme(theme: Theme) {
  document.documentElement.dataset.theme = theme;
}

// 別のウィンドウや Rust で theme が変わったら反映する
const unlisten = await settings.onKeyChange<Theme>('theme', (value) => applyTheme(value ?? 'system'));
applyTheme(await getTheme());

export async function acceptLicense() {
  await settings.set('licenseAccepted', true);
  await settings.save(); // 自動保存を待たずに書き込む
}

主なメソッドは次のとおりです。

メソッド動き
has() / delete()キーの有無の確認・削除
keys() / entries() / length()一覧と件数
reset()defaults の状態に戻す
clear()すべて消す(既定値も消える)
reload()ファイルを読み直す(外で編集されたとき)

2. バックエンドから実装する (Rust)

画面より先に使いたい設定は、setup で StoreExt の store_builder() から読みます。setup はページより先に動くので、ここで作ったストアに JS の defaults は効きません。Rust で先に開くなら、既定値も Rust 側で決めます。Rust の set() も JS の onKeyChange() に届きます。

use serde_json::json;
use std::time::Duration;
use tauri::Manager;
use tauri_plugin_store::StoreExt;

const SETTINGS: &str = "settings.json";

/// 最近開いたファイルの先頭に追加する(最大 10 件)
#[tauri::command]
fn add_recent_file(app: tauri::AppHandle, path: String) -> Result<Vec<String>, String> {
    let store = app.store(SETTINGS).map_err(|e| e.to_string())?;
    let mut recent: Vec<String> = store
        .get("recentFiles")
        .and_then(|v| serde_json::from_value(v).ok())
        .unwrap_or_default();
    recent.retain(|p| p != &path);
    recent.insert(0, path);
    recent.truncate(10);
    store.set("recentFiles", json!(recent));
    Ok(recent)
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_store::Builder::default().build())
        .setup(|app| {
            let store = app
                .store_builder(SETTINGS)
                .default("theme", "system")
                .default("fontSize", 14)
                .default("recentFiles", json!([]))
                .auto_save(Duration::from_millis(500))
                .build()?;
            // 保存したテーマをタイトルバーなどに反映する("system" は OS に合わせる)
            let theme = match store.get("theme").as_ref().and_then(|v| v.as_str()) {
                Some("dark") => Some(tauri::Theme::Dark),
                Some("light") => Some(tauri::Theme::Light),
                _ => None,
            };
            if let Some(win) = app.get_webview_window("main") {
                win.set_theme(theme)?;
            }
            Ok(())
        })
        .invoke_handler(tauri::generate_handler![add_recent_file])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
import { invoke } from '@tauri-apps/api/core';

const recent = await invoke<string[]>('add_recent_file', { path: 'C:\\work\\report.csv' });
console.log(recent); // ['C:\\work\\report.csv', ...]

動作確認

npm run tauri dev で起動し、ボタンなどから setTheme('dark') を呼びます。0.5 秒ほどで、Windows なら %APPDATA%\<identifier>\settings.json に次のように書かれます(キーの並びは保存のたびに変わることがあります)。

{
  "fontSize": 14,
  "recentFiles": [],
  "theme": "dark"
}

同じページのウィンドウを 2 つ開いて片方でテーマを変えると、もう片方も onKeyChange() で切り替わります。再起動後も getTheme() は dark を返します。

よくあるエラーと対処法

  • 「store.load not allowed. Permissions associated with this command: store:allow-load, store:default」: 権限の追加漏れです(リリースビルドでは「Command plugin:store|load not allowed by ACL」)。store:default を足して再起動します。
  • 「plugin store not found」: lib.rs で .plugin(tauri_plugin_store::Builder::default().build()) を登録していません。
  • 設定がすべて既定値に戻った: ファイルの JSON が壊れていると、エラーにならず既定値で開き、次の保存で壊れたファイルは上書きされます。手で編集する運用なら、事前に写しを取るよう案内します。
  • defaults や autoSave が効かない: そのパスのストアを先にほかのウィンドウか Rust が開いています。オプションを 1 か所にまとめます。

OS ごとの違いと注意点

  • 保存場所: 相対パスは appDataDir() の下で、Windows は AppData\Roaming、macOS は ~/Library/Application Support、Linux は ~/.local/share の <identifier> フォルダーです。Linux で ~/.config に置きたいなら、appConfigDir() と join() で作った絶対パスを渡します(fs-018)。
  • 平文: 誰でも読めるテキストなので、秘密の値は Stronghold に分けます。
  • 大きなデータに向かない: 全体をメモリに持ち、保存のたびにファイル全体を書き直します。
  • アプリを 2 つ起動したとき: それぞれが自分のメモリの内容で上書きし合い、後から保存した方が残ります。
  • set_theme(): Linux と macOS ではアプリ全体に効き、iOS / Android では使えません。

関連レシピ

参考リンク(公式ドキュメント)

Web Ninja

この記事を書いた人

Web Ninja ウェブエンジニア (Web Engineer)

会社員ネットワークエンジニアから独立してかれこれ 25 年以上 Web エンジニアとして活動中。普段は JavaScript と Node.js を自在に操り、時には C++ や Perl といった古流の技も嗜みます。近年は Tauri × Rust という新たな武器を手に、デスクトップアプリ開発の最前線を駆け抜けています。「作りたい」を「作れる」に変えるための、実践的な「技」をお届けします。

お問い合わせ: tauri.ninja@gmail.com

内容の誤り・動かないコードを報告する