npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

tauri-plugin-sync-state

v0.1.0

Published

Bidirectional, type-keyed state sync between a Tauri backend and a JS/React frontend.

Readme

tauri-plugin-sync-state

Bidirectional, type-keyed state synchronization between a Tauri v2 backend and a JS/React frontend.

  • Single source of truth in Rust. State lives in the backend.
  • Reactive frontend. A React hook re-renders on every backend change — including changes the backend makes on its own (timers, sockets, tasks).
  • Writable from the frontend. set/patch push to the backend, which broadcasts the new value back to every window.
  • Multiple slices, separated by type name — no hand-written string ids.
  • #[derive(SyncState)] for zero-boilerplate registration.
  • Mutator<S> injects into commands like Tauri's own State<T>, with field reads via Deref and sync/async mutation.

How it works

Each state struct is a slice, identified by its type name (AppState"AppState"). That name is the registry key, the IPC argument, and the suffix of a per-slice event channel plugin:sync-state:updated:{Name}, so a listener for one slice never wakes on another's change.

Rust struct  ──register──►  StateRegistry (one source of truth)
   ▲   │                         │
   │   │ set_state (invoke)      │ emit "…updated:{Name}"
   │   ▼                         ▼
  React hook  ◄────────────  frontend listener → re-render

Install

Rust (src-tauri/Cargo.toml)

[dependencies]
tauri-plugin-sync-state = "0.1"
serde = { version = "1", features = ["derive"] }

JavaScript

npm install tauri-plugin-sync-state
# or: pnpm add tauri-plugin-sync-state

Permissions (src-tauri/capabilities/default.json)

{
    "permissions": [
        "sync-state:default"
    ]
}

sync-state:default allows get_state and set_state.


Quick start

1. Define and register slices (Rust)

use serde::{Deserialize, Serialize};
use tauri_plugin_sync_state::{Builder, Mutator, SyncState};

#[derive(SyncState, Serialize, Deserialize, Clone, Default)]
struct AppState {
    count: i32,
    username: String
}

#[derive(SyncState, Serialize, Deserialize, Clone, Default)]
struct UserPrefs {
    theme: String,
    notifications: bool
}

fn main() {
    tauri::Builder::default()
        .plugin(
            Builder::new()
                .register(AppState::default())
                .register(UserPrefs::default())
                .build(),
        )
        .invoke_handler(tauri::generate_handler![increment, go_online, whoami])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

2. Read and mutate from commands

Mutator<S> resolves automatically. Reads go through Deref; mutating methods take &mut self, so mutating commands bind mut.

// read-only — no `mut`
#[tauri::command]
fn whoami(app_state: Mutator<AppState>) -> String {
    app_state.username.clone()        // Deref field access
}

// mutate — `mut` bindings; both params auto-injected
#[tauri::command]
fn go_online(mut prefs: Mutator<UserPrefs>, mut app_state: Mutator<AppState>) {
    prefs.mutate(|p| p.notifications = true);
    app_state.mutate(|s| s.username = "Kobe".into());
}

// mix with normal args
#[tauri::command]
fn increment(by: i32, mut app_state: Mutator<AppState>) {
    app_state.mutate(|s| s.count += by);
}

Async mutation (mutate_async runs your future without holding the lock, then commits and broadcasts):

#[tauri::command]
async fn sync_username(mut app_state: Mutator<AppState>) {
    app_state.mutate_async(|mut s| async move {
        s.username = fetch_username_from_server().await;
        s
    }).await;
}

3. Mutate from non-command Rust (timers, sockets, tasks)

use tauri_plugin_sync_state::SyncStateExt;

// anywhere you hold an AppHandle:
app.sync_mutate::<AppState>( | s| s.count += 1).ok();   // updates + broadcasts
let current = app.sync_read::<AppState>() ?;            // typed read

4. Use it in React

Declare each slice's hook once:

// src/state/index.ts
import { createSyncStateHook } from "tauri-plugin-sync-state/react";

interface AppState {
    count: number;
    username: string
}

interface UserPrefs {
    theme: string;
    notifications: boolean
}

export const useAppState = createSyncStateHook<AppState>("AppState");
export const useUserPrefs = createSyncStateHook<UserPrefs>("UserPrefs");

Then consume anywhere — it stays in sync with the backend automatically:

import { useAppState, useUserPrefs } from "./state";

function Counter() {
    const {state, patch} = useAppState();
    if (!state) return null;
    return (
        <button onClick={() => patch({count: state.count + 1})}>
            count: {state.count}
        </button>
    );
}

function ThemeToggle() {
    const {state, patch} = useUserPrefs();
    return (
        <button onClick={() => patch({theme: state?.theme === "dark" ? "light" : "dark"})}>
            theme: {state?.theme ?? "…"}
        </button>
    );
}

The hook hydrates on mount, re-renders on every backend update (including backend-initiated ones), and set/patch push to the backend optimistically.


API reference

Rust

| Item | Description | |-------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------| | #[derive(SyncState)] | Implements SyncState, using the type name as NAME. Override with #[sync_state(name = "…")]. Requires Serialize + Deserialize + Clone. | | Builder::new().register(S::default()).build() | Builds the plugin; register one slice per type. | | Mutator<S> | Command-injectable handle. Derefs to S for reads. | | Mutator::mutate(\|s\| …) | Sync mutate + broadcast (logs on error). | | Mutator::try_mutate(…) | Fallible mutate. | | Mutator::mutate_async(\|s\| async { … s }).await | Async mutate + broadcast. | | Mutator::reload() | Re-pull latest into the snapshot (for long commands). | | AppHandle::sync_read::<S>() / sync_mutate::<S>(…) | Read / mutate from non-command code (SyncStateExt). |

JavaScript

| Import | Description | |-------------------------------------------|-----------------------------------------| | getState<S>(name) | Fetch current value. | | setState<S>(name, value) | Replace value (backend broadcasts). | | onStateUpdated<S>(name, cb) | Subscribe; returns an unlisten promise. | | useSyncState<S>(name, fallback?) | Hook → { state, set, patch }. | | createSyncStateHook<S>(name, fallback?) | Returns a zero-arg bound hook. |


Notes & guarantees

  • Identity: the JS string must equal the Rust NAME. A mismatch produces a clear NotFound error at runtime, not a silent wrong-type result.
  • Consistency: one mutex per registry; last-write-wins. mutate_async does not hold the lock across .await.
  • Snapshot semantics: a Mutator's Deref value is captured at command dispatch and refreshed by its own mutations; call reload() to observe concurrent external writes mid-command.
  • patch is a shallow merge. For nested updates, read state, build the new object, and call set.
  • Don't call back into the registry from inside a mutate closure (re-entrant lock).

This project is licensed under the MIT License. See the LICENSE file for details.

Agentically created by Claude Opus 4.8