A lightweight Electron state synchronization library that enables seamless data sharing between main and renderer processes. Supports React, Vue, Svelte, SolidJS, Zustand, TanStack Query, Jotai, and Redux Toolkit with automatic multi-window sync.
npm install electron-state-sync
// main.ts
import { state } from "electron-state-sync/main";
const counter = state({
name: "counter",
initialValue: 0,
});
counter.set(10);
const value = counter.get();
// main.ts
import { state } from "electron-state-sync/main";
const counter = state({
baseChannel: "state",
name: "counter",
initialValue: 0,
allowRendererSet: true,
resolveRendererValue: (value) => {
if (typeof value !== "number") {
throw new Error("counter only accepts number");
}
return value;
},
});
counter.set(100);
const current = counter.get();
All windows automatically receive updates when state changes:
// main.ts
import { state } from "electron-state-sync/main";
const theme = state({
name: "theme",
initialValue: "light",
});
// All windows using this state will receive updates
theme.set("dark"); // Broadcast to all subscribed windows
Call dispose() to stop syncing and clean up IPC handlers:
// main.ts
import { state } from "electron-state-sync/main";
const counter = state({
name: "counter",
initialValue: 0,
});
counter.set(10); // Sync and broadcast
counter.get(); // Returns 10
// Stop syncing - removes IPC handlers and clears subscribers
counter.dispose();
After dispose() is called:
get/set/subscribe/unsubscribe are removedEach window subscribes to state changes and receives automatic updates:
// renderer process
import { useSyncState } from "electron-state-sync/react";
const [theme] = useSyncState("light", {
name: "theme",
});
// When any window calls theme.set(), all windows update automatically
// preload.ts
import { exposeSyncState } from "electron-state-sync/preload";
exposeSyncState();
Browser exposes window.syncState with get / set / subscribe:
// renderer process
const bridge = window.syncState;
if (!bridge) {
throw new Error("syncState not injected");
}
const value = await bridge.get<number>({
baseChannel: "state",
name: "counter",
});
await bridge.set(
{
baseChannel: "state",
name: "counter",
},
value + 1
);
const unsubscribe = bridge.subscribe<number>(
{
baseChannel: "state",
name: "counter",
},
(nextValue) => {
console.log(nextValue);
}
);
You can implement SyncStateBridge for custom integration:
// renderer process
import type { SyncStateBridge } from "electron-state-sync/renderer";
const customBridge: SyncStateBridge = {
get: async (options) => window.syncState!.get(options),
set: async (options, value) => window.syncState!.set(options, value),
subscribe: (options, listener) => window.syncState!.subscribe(options, listener),
};
// renderer process
import { useSyncState } from "electron-state-sync/vue";
const counter = useSyncState(0, {
name: "counter",
});
// counter.isSynced - Ref<boolean>
// renderer process
import { initSyncState, useSyncState } from "electron-state-sync/vue";
initSyncState({
baseChannel: "myapp",
});
const counter = useSyncState(0, {
name: "counter",
});
const user = useSyncState({ name: "" }, {
name: "user",
});
const theme = useSyncState("light", {
baseChannel: "settings",
name: "theme",
});
// renderer process
import { useSyncState } from "electron-state-sync/vue";
const counter = useSyncState(0, {
name: "counter",
bridge: customBridge,
deep: false,
});
// renderer process
import { useSyncState } from "electron-state-sync/react";
function App() {
const [counter, setCounter, isSynced] = useSyncState(0, {
name: "counter",
});
return <div onClick={() => setCounter(5)}>{counter}</div>;
}
// renderer process
import { initSyncState, useSyncState } from "electron-state-sync/react";
initSyncState({
baseChannel: "myapp",
});
const [counter, setCounter] = useSyncState(0, {
name: "counter",
});
const [user, setUser] = useSyncState({ name: "" }, {
name: "user",
});
const [theme, setTheme] = useSyncState("light", {
baseChannel: "settings",
name: "theme",
});
// renderer process
import { useSyncState } from "electron-state-sync/react";
const [counter, setCounter] = useSyncState(0, {
name: "counter",
bridge: customBridge,
});
// renderer process
import { useSyncState } from "electron-state-sync/svelte";
const counter = useSyncState(0, {
name: "counter",
});
// counter.isSynced - Readable<boolean>
// renderer process
import { initSyncState, useSyncState } from "electron-state-sync/svelte";
initSyncState({
baseChannel: "myapp",
});
const counter = useSyncState(0, {
name: "counter",
});
const user = useSyncState({ name: "" }, {
name: "user",
});
const theme = useSyncState("light", {
baseChannel: "settings",
name: "theme",
});
// renderer process
import { useSyncState } from "electron-state-sync/svelte";
const counter = useSyncState(0, {
name: "counter",
bridge: customBridge,
});
<script lang="ts">
import { counter } from "./stores";
</script>
<div>{$counter}</div>
// renderer process
import { useSyncState } from "electron-state-sync/solid";
const [counter, setCounter, isSynced] = useSyncState(0, {
name: "counter",
});
// renderer process
import { initSyncState, useSyncState } from "electron-state-sync/solid";
initSyncState({
baseChannel: "myapp",
});
const [counter, setCounter] = useSyncState(0, {
name: "counter",
});
const [user, setUser] = useSyncState({ name: "" }, {
name: "user",
});
const [theme, setTheme] = useSyncState("light", {
baseChannel: "settings",
name: "theme",
});
// renderer process
import { useSyncState } from "electron-state-sync/solid";
const [counter, setCounter] = useSyncState(0, {
name: "counter",
bridge: customBridge,
});
// renderer process
import { create } from "zustand";
import { syncStateMiddleware } from "electron-state-sync/zustand";
const useStore = create(
syncStateMiddleware({ name: "counter" })((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}))
);
// In component
const count = useStore((state) => state.count);
// renderer process
import { initSyncState } from "electron-state-sync/zustand";
import { create } from "zustand";
import { syncStateMiddleware } from "electron-state-sync/zustand";
initSyncState({
baseChannel: "myapp",
});
const useStore = create(
syncStateMiddleware({ name: "counter" })((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}))
);
// renderer process
import { create } from "zustand";
import { syncStateMiddleware } from "electron-state-sync/zustand";
const useStore = create(
syncStateMiddleware({
name: "counter",
bridge: customBridge,
})((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}))
);
// renderer process
import { useSyncState } from "electron-state-sync/react-query";
function App() {
const { data: count, isSynced, update } = useSyncState(0, {
name: "counter",
});
return <div onClick={() => update(5)}>{count}</div>;
}
// renderer process
import { initSyncState, useSyncState } from "electron-state-sync/react-query";
initSyncState({
baseChannel: "myapp",
});
function App() {
const { data: count, isSynced, update } = useSyncState(0, {
name: "counter",
});
return <div onClick={() => update(5)}>{count}</div>;
}
// renderer process
import { useSyncState } from "electron-state-sync/react-query";
function App() {
const { data: count, isSynced, update } = useSyncState(0, {
name: "counter",
bridge: customBridge,
});
return <div onClick={() => update(5)}>{count}</div>;
}
// renderer process
import { atom, useAtom } from "jotai";
import { syncStateAtom } from "electron-state-sync/jotai";
const countAtom = syncStateAtom(0, { name: "counter" });
function App() {
const [count, setCount] = useAtom(countAtom);
return <div onClick={() => setCount(5)}>{count}</div>;
}
// renderer process
import { initSyncState } from "electron-state-sync/jotai";
import { atom, useAtom } from "jotai";
import { syncStateAtom } from "electron-state-sync/jotai";
initSyncState({
baseChannel: "myapp",
});
const countAtom = syncStateAtom(0, { name: "counter" });
function App() {
const [count, setCount] = useAtom(countAtom);
return <div onClick={() => setCount(5)}>{count}</div>;
}
// renderer process
import { atom, useAtom } from "jotai";
import { syncStateAtom } from "electron-state-sync/jotai";
const countAtom = syncStateAtom(0, {
name: "counter",
bridge: customBridge,
});
function App() {
const [count, setCount] = useAtom(countAtom);
return <div onClick={() => setCount(5)}>{count}</div>;
}
// renderer process
import { configureStore, createSlice } from "@reduxjs/toolkit";
import { syncStateMiddleware } from "electron-state-sync/redux";
import { Provider, useDispatch, useSelector } from "react-redux";
const counterSlice = createSlice({
name: "counter",
initialState: { value: 0 },
reducers: {
setValue: (state, action) => {
state.value = action.payload;
},
},
});
const store = configureStore({
reducer: {
counter: counterSlice.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(
syncStateMiddleware({
name: "counter",
selector: (state) => state.counter.value,
actionType: "counter/setValue",
})
),
});
function App() {
const count = useSelector((state) => state.counter.value);
const dispatch = useDispatch();
return <div onClick={() => dispatch(counterSlice.actions.setValue(5))}>{count}</div>;
}
// renderer process
import { initSyncState } from "electron-state-sync/redux";
import { configureStore, createSlice } from "@reduxjs/toolkit";
import { syncStateMiddleware } from "electron-state-sync/redux";
initSyncState({
baseChannel: "myapp",
});
const counterSlice = createSlice({
name: "counter",
initialState: { value: 0 },
reducers: {
setValue: (state, action) => {
state.value = action.payload;
},
},
});
const store = configureStore({
reducer: {
counter: counterSlice.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(
syncStateMiddleware({
name: "counter",
selector: (state) => state.counter.value,
actionType: "counter/setValue",
})
),
});
// renderer process
import { configureStore, createSlice } from "@reduxjs/toolkit";
import { syncStateMiddleware } from "electron-state-sync/redux";
const counterSlice = createSlice({
name: "counter",
initialState: { value: 0 },
reducers: {
setValue: (state, action) => {
state.value = action.payload;
},
},
});
const store = configureStore({
reducer: {
counter: counterSlice.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(
syncStateMiddleware({
name: "counter",
selector: (state) => state.counter.value,
actionType: "counter/setValue",
bridge: customBridge,
})
),
});
Channel format: ${baseChannel}:${name}:get|set|subscribe|unsubscribe|update.
Vue Only: Deep watch is only supported in Vue integration.
Enable deep watch when value is object (Vue only):
// Vue example
const profile = useSyncState(
{ name: "Alice" },
{
name: "profile",
deep: true, // Only available in Vue
}
);
Note:
Framework bundles (ESM / CJS):
| Package | ESM | CJS | gzip |
|---|---|---|---|
| Main | 6.44 kB | 6.51 kB | 1.95 kB |
| Preload | 1.49 kB | 1.54 kB | 0.49 kB |
| Zustand | 5.88 kB | 6.06 kB | 1.43 kB |
| Redux | 4.37 kB | 4.54 kB | 1.34 kB |
| React Query | 3.34 kB | 3.53 kB | 1.13 kB |
| Jotai | 3.32 kB | 3.44 kB | 1.14 kB |
| Vue | 2.24 kB | 2.25 kB | 0.81 kB |
| Solid | 2.21 kB | 2.24 kB | 0.77 kB |
| Svelte | 1.77 kB | 1.82 kB | 0.64 kB |
| Preact | 1.43 kB | 1.51 kB | 0.56 kB |
| React | 1.42 kB | 1.45 kB | 0.55 kB |
Framework Integration (choose as needed):
State Management Integration (choose as needed):
MIT