Last active
October 7, 2024 06:09
-
-
Save anis-dr/5cba43157b87ecab19e59bd8fecca638 to your computer and use it in GitHub Desktop.
Zustand middleware to sync state with electron main proces
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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