Store Plugin - Persistent Key-Value Storage in Tauri v2

How to use the Tauri store plugin to save and load application data across sessions with a simple key-value file. Covers installation, creating stores, reading and writing data, auto-save, LazyStore, and best practices.

The Store plugin gives your Tauri app a lightweight, persistent key-value store that survives application restarts. Think of it as a file‑based localStorage that works from both your React frontend and Rust backend — data is written to disk as JSON, so it’s easy to inspect and debug. This is the right tool for saving user preferences, window states, theme choices, or any small‑to‑medium‑sized configuration that doesn’t need a full database.

State management overview:

The store is one of several ways to handle state in Tauri. For larger structured data, the SQL Plugin may be a better fit. See the plugin introduction for context on when to pick which tool.

Supported platforms

The store plugin works on all Tauri targets — Windows, macOS, Linux, Android, and iOS. It requires Rust 1.77.2 or later.

Installation

You can add the plugin with a single automatic command that handles the Rust crate and updates permissions, or you can install each piece manually. The same four-step pattern is documented in Installing Plugins.

Run the CLI helper from your preferred package manager. It adds the Rust dependency, registers the plugin, and grants the default permissions in one step.

npm run tauri add store

What this does:

The tauri add command edits src-tauri/Cargo.toml, inserts .plugin(...) into src-tauri/src/lib.rs, and appends "store:default" to your capabilities file. After it completes, the store is ready to use.


Creating a store

A store is identified by a filename (e.g., "settings.json" or "user-preferences.dat"). The file lives in the app’s local data directory — a platform‑specific folder that Tauri manages for you. You can create multiple stores for different concerns: one for UI preferences, another for cached data, and so on. Creating a Store covers load options, LazyStore, and save strategies in isolation.

From the frontend (JavaScript)

In your React code, import the load function to open or create a store. The first call to load with a given filename creates the store in memory; subsequent calls (even from Rust) with the same filename reuse the same instance.

import { useEffect, useState } from "react";
import { load } from "@tauri-apps/plugin-store";

function App() {
  const [theme, setTheme] = useState<string>("light");

  useEffect(() => {
    const init = async () => {
      const store = await load("settings.json", { autoSave: false });
      const saved = await store.get<{ value: string }>("theme");
      if (saved) setTheme(saved.value);
    };
    init();
  }, []);

  const toggleTheme = async () => {
    const newTheme = theme === "light" ? "dark" : "light";
    setTheme(newTheme);
    const store = await load("settings.json");
    await store.set("theme", { value: newTheme });
    await store.save(); // manual save because autoSave is off
  };

  return (
    <div>
      <p>Current theme: {theme}</p>
      <button onClick={toggleTheme}>Toggle theme</button>
    </div>
  );
}

export default App;

The load function returns a Store instance. The second argument is an options object where you can set autoSave:

  • autoSave: false — you must call store.save() yourself.
  • autoSave: true (or just omit it) — the store writes to disk after a short debounce delay (100 ms by default) whenever a value changes. This is the default.
  • autoSave: number — use a custom debounce delay in milliseconds.

Forgetting save when autoSave is off:

If you disable auto‑save and never call store.save(), data written during a session is lost when the app closes (unless it closes gracefully — Tauri flushes stores on exit, but relying on that is fragile). Always make your save strategy explicit.

The lazy alternative

If your store file is large or you want to defer the disk I/O until the first read or write, use LazyStore. It loads data from disk only when you actually touch it.

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

const store = new LazyStore("settings.json");
// No file read happens here.

const theme = await store.get("theme"); // loads from disk now

LazyStore works exactly like the regular Store from that point on. It’s a convenient choice for stores that might not be used in every session.

From Rust

You can also create and manipulate a store directly in your Rust backend, which is useful for initialization logic or for data that the frontend doesn’t touch directly.

use tauri::Manager;
use tauri_plugin_store::StoreExt;
use serde_json::json;

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_store::Builder::new().build())
        .setup(|app| {
            // Create or open a store — stored in the app's resource table
            let store = app.store("app_data.json")?;

            // Values must be serde_json::Value for JS interop
            store.set("launch_count".to_string(), json!(1));

            Ok(())
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

app.store("filename.json") creates the store, loads its content from disk, and registers it in the resource table so the same instance is shared with JavaScript calls using the same filename. After you’re done, you can remove the store from the table with store.close_resource() to free memory.

serde_json::Value is mandatory for cross-language access:

If you call store.set() from Rust with any type other than serde_json::Value, the JavaScript side will not be able to read it — store.get() will return null or an unexpected value. Always wrap your data with json!( ... ).


Working with data

The store exposes a full set of key‑value operations. All of them are asynchronous in JavaScript because the data lives on the Rust side and must cross the IPC bridge. See Working with Data for set, get, delete, and change listeners.

Basic CRUD operations

import { load } from "@tauri-apps/plugin-store";

const store = await load("data.json");

// Write
await store.set("username", "alice");
await store.set("session", { token: "abc123", expires: 1712345678 });

// Read — get returns null if the key doesn't exist
const username = await store.get<string>("username");
console.log(username); // "alice"

// Check existence
const hasSession = await store.has("session"); // true

// Delete a single key
await store.delete("session");

// Remove all keys (clear the store)
await store.clear();

// Reset — clears the in‑memory state AND deletes the file on disk
await store.reset();

The generic parameter on get (e.g., get<string>) provides TypeScript type safety. If the stored value doesn’t match the expected shape, the call still succeeds but the returned object won’t conform to the type — so always validate critical data.

Bulk operations

You can iterate over keys, values, or entries without loading the entire store into your component state individually.

const keys = await store.keys();       // string[]
const values = await store.values();   // unknown[]
const entries = await store.entries(); // Array<[string, unknown]>
const count = await store.length();    // number

These are helpful for building settings pages that list all available preferences without hard‑coding every key.

Saving and loading explicitly

Even when autoSave is enabled, there are times you want to force a disk write or refresh from disk.

// Force a write to disk immediately
await store.save();

// Reload the store from disk (discards unsaved changes!)
await store.reload();

reload replaces the in‑memory state with whatever is on disk. If there were unsaved changes, they are lost — treat reload as a “revert to saved” operation.

reload discards in‑memory changes:

Calling await store.reload() throws away any data you have set since the last save. It does not warn you. If your app needs a “revert” feature, reload is the right tool; otherwise, avoid it unless you know exactly why you’re discarding the current state.

A complete React example: saving and loading a counter

This component shows the full lifecycle: creating a store, reading a counter on mount, incrementing on button click, and persisting with manual save.

import { useState, useEffect } from "react";
import { load } from "@tauri-apps/plugin-store";

export default function Counter() {
  const [count, setCount] = useState(0);
  const [status, setStatus] = useState("loading");

  useEffect(() => {
    (async () => {
      try {
        const store = await load("counter.json", { autoSave: false });
        const saved = await store.get<number>("count");
        if (saved !== null) {
          setCount(saved);
        }
        setStatus("ready");
      } catch {
        setStatus("error");
      }
    })();
  }, []);

  const increment = async () => {
    const next = count + 1;
    setCount(next);
    try {
      const store = await load("counter.json");
      await store.set("count", next);
      await store.save();
    } catch (e) {
      console.error("Save failed:", e);
    }
  };

  if (status === "error") return <p>Failed to load store.</p>;
  if (status === "loading") return <p>Loading…</p>;

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>+1</button>
    </div>
  );
}

First‑run load can fail:

If the store file does not exist yet (the very first launch), calling load or reload may throw an error. In the example above, we catch that and fall back to a default value. You can also check with store.has("key") before reading, but catching the error is the simplest guard.


Best practices

The Store Plugin best practices page expands on save strategy, store layout, and permissions.

Choose the right save strategy

Use caseRecommended autoSave
Infrequent, important changes (settings save button)false — save manually on explicit user action
Rapid changes (slider position, typing indicator)a debounce value like 1000 (1 second) to avoid thrashing the disk
Default behaviour — safe for most appstrue (100 ms debounce) — balances responsiveness and performance
The default auto‑save writes after the last change in a 100 ms window. For most apps this is fine, but a debounce that’s too short can cause unnecessary I/O if many keys are set in quick succession.

Organize store files by domain

Create separate store files instead of stuffing everything into one giant blob. For example:

  • ui-settings.json — theme, font size, sidebar collapsed state
  • auth-session.json — tokens, expiry (consider encryption for secrets)
  • user-preferences.json — app‑specific preferences This keeps each file small, reduces I/O contention, and makes it easier to reason about what is stored where.

Handle errors gracefully

Every store operation can fail — the disk may be full, permissions may be missing, or the file may be corrupted. Always wrap store calls in try/catch (JS) or proper Result handling (Rust). Provide fallback defaults so your app doesn’t crash.

Avoid storing secrets in plain text

The store file is readable JSON on disk. Do not store passwords, API keys, or authentication tokens there without encryption. For sensitive data, use platform‑provided secure storage or a dedicated secrets plugin.

Use the same store instance

When you call load("settings.json") from different components, you get a reference to the same underlying store. That means a set in one part of the app is immediately visible to a get elsewhere — no manual synchronization needed.

Migrating from Tauri v1

If you’re coming from Tauri v1, the API surface has changed:

// v1
import { Store } from "tauri-plugin-store-api";
const store = new Store(".settings.dat");

// v2
import { load } from "@tauri-apps/plugin-store";
const store = await load(".settings.dat");

In Rust, the older with_store closure has been replaced by StoreExt methods (app.store(path)) that return the store directly.

Check your setup:

After installation, the simplest way to confirm everything works is to run a quick smoke test: await load("test.json") followed by await store.set("health", "ok") — if no error appears in the console, the plugin is wired correctly.

Store Plugin Introduction

An overview of the Tauri Store Plugin covering what it is, the problems it solves, and its core concepts

Creating a Store

Understand how to create persistent key-value stores with the Store plugin in Tauri v2, including store files, loading, and saving mechanisms.

Working with Data

Learn how to set, read, update, and remove persistent data using the Tauri Store Plugin in a React and Vite application

Best Practices for the Store Plugin

Guidelines for using the Tauri v2 store plugin effectively in a React and Vite application to manage configuration, user preferences, and persistent data without introducing bugs or performance problems.