Skip to content

Instantly share code, notes, and snippets.

@anis-dr
Last active October 7, 2024 06:09
Show Gist options
  • Save anis-dr/5cba43157b87ecab19e59bd8fecca638 to your computer and use it in GitHub Desktop.
Save anis-dr/5cba43157b87ecab19e59bd8fecca638 to your computer and use it in GitHub Desktop.
Zustand middleware to sync state with electron main proces
import { UseBoundStore } from 'zustand/esm';
import { StoreApi } from 'zustand';
import type { ElectronSyncOptions } from './middleware';
export function getSerializableState(excludes: string[], state: unknown) {
return JSON.parse(
JSON.stringify(state, (key, value) => {
if (typeof value === 'function' || excludes.includes(key)) {
return undefined;
}
return value;
})
);
}
function receiveStateFromMain<T>(
store: StoreApi<T>,
options: ElectronSyncOptions
) {
if (typeof window !== 'undefined') {
window.electron.ipcRenderer.on<{
key: string;
state: Awaited<typeof store.getState>;
}>('zustand-sync-renderer', ({ key, state }) => {
if (key === options.key) {
store.setState(state);
}
});
}
}
function sendStateToMain<T>(excludes: string[], get: () => T, key: string) {
try {
const rawState = getSerializableState(excludes, get());
window.electron.ipcRenderer.sendMessage('zustand-sync', {
key,
state: rawState,
});
} catch (error) {
console.error(error);
}
}
function sendStateToRenderer<T>(excludes: string[], get: () => T, key: string) {
try {
const rawState = getSerializableState(excludes, get());
// eslint-disable-next-line global-require
const { BrowserWindow } = require('electron');
BrowserWindow.getAllWindows().forEach((win) => {
if (win.webContents) {
win.webContents.send('zustand-sync-renderer', {
key,
state: rawState,
});
}
});
} catch (error) {
console.error(error);
}
}
function receiveStateFromRenderer(store: UseBoundStore<StoreApi<unknown>>) {
if (typeof window !== 'undefined')
throw new Error('This function is for main process only');
// eslint-disable-next-line global-require
const { ipcMain } = require('electron');
// const listeners = ipcMain.listenerCount('zustand-sync');
// if (listeners !== 0) return;
ipcMain?.on('zustand-sync', (_, args) => {
const { state, key } = args[0];
if (store.key === key) {
store.setState(state);
}
});
}
export {
receiveStateFromMain,
receiveStateFromRenderer,
sendStateToMain,
sendStateToRenderer,
};
import { State, StateCreator, StoreMutatorIdentifier } from 'zustand';
import {
receiveStateFromMain,
sendStateToMain,
sendStateToRenderer,
} from './helpers';
declare module 'zustand' {
interface StoreApi<T extends State> {
key: string;
}
interface StoreMutators<S, A> {
setKey: Write<Cast<S, object>, { key: string }>;
}
}
type Write<T extends object, U extends object> = Omit<T, keyof U> & U;
type Cast<T, U> = T extends U ? T : U;
type UnionOfObjectKeysWithoutFunctions<T> = T extends object
? // eslint-disable-next-line @typescript-eslint/ban-types
{ [K in keyof T]: T[K] extends Function ? never : K }[keyof T]
: never;
type ElectronSyncInputOptions<T extends State> = {
key: string;
excludes?: UnionOfObjectKeysWithoutFunctions<T>[];
};
type ElectronSyncWithExcludes = <
T extends State,
Mps extends [StoreMutatorIdentifier, unknown][] = [],
Mcs extends [StoreMutatorIdentifier, unknown][] = []
>(
f: StateCreator<T, Mps, Mcs>,
options: ElectronSyncInputOptions<T>
) => StateCreator<T, Mps, Mcs>;
export type ElectronSyncOptions = {
key: string;
excludes?: string[];
};
type ElectronSyncImpl = <T extends State>(
f: StateCreator<T, [], []>,
options: ElectronSyncOptions
) => StateCreator<T, [], []>;
const electronSyncImpl: ElectronSyncImpl =
(f, options) => (set, get, _store) => {
const store = _store as any;
const { key, excludes = [] } = options;
if (!store.key) store.key = key;
if (typeof window !== 'undefined') {
if (store.key === key) {
receiveStateFromMain(store, options);
}
}
const electronSyncSet: typeof set = (...a) => {
set(...a);
if (store.key === key) {
if (typeof window !== 'undefined') {
sendStateToMain(excludes, get, key);
} else {
sendStateToRenderer(excludes, get, key);
}
}
};
return f(electronSyncSet, get, store);
};
const electronSync = electronSyncImpl as unknown as ElectronSyncWithExcludes;
export default electronSync;
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
import electronSync from './middleware';
type FishState = {
fish: number;
increasePopulation: () => void;
removeAllFish: () => void;
};
export const useFishStore = create<FishState, [['zustand/devtools', never]]>(
electronSync(
devtools(
(set) => ({
fish: 0,
increasePopulation: () =>
set(
(state) => ({ fish: state.fish + 1 }),
false,
'increasePopulation'
),
removeAllFish: () => set({ fish: 0 }),
}),
{
name: 'Fish Store',
}
),
{ key: 'fish-store', excludes: [] }
)
);
export default useFishStore;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment