Share state between Electron renderers and main processes
Sharing state between multiple renderers shouldn't be hard. So we fixed it by adding simple React hooks similar to useState
.
Example:
export function Settings() {
const [settings, setSettings] = useSettings('test', [])
const add = () => setSettings([...settings, 'entry'])
return (
<div>
<button onClick={add}>Add</button>
{settings.map((s, i) => (
<div key={`entry-${i}`}>{s}</div>
))}
</div>
)
}
First, install via yarn or npm npm install electron-shared-state-react
to your electron application.
There are two sets of managers:
-
SettingsManager
With this manager you can persist to disk a set of settings. This is great to store application state.
-
GlobalStateManager
With this manager you can syncronize runtime state between the renderer and main processes, but this data will not be persisted.
Ensure that you also dispose these properly at the end of your application lifecycle in your electron main
process.
import {
GlobalSharedStateManager,
SettingsManager,
} from 'electron-shared-state-react/dist/main'
app.on('ready', async () => {
...
await SettingsManager.ready()
GlobalSharedStateManager.ready()
...
}
app.on('before-quit', async () => {
...
SettingsManager.quit()
GlobalSharedStateManager.quit()
...
}
We need to expose three methods via Electron's content bridge, so we can communicate with our main process.
preload.ts
Example
import { contextBridge, ipcRenderer } from 'electron'
import {
Platform,
StateChannel,
} from 'electron-shared-state-react/dist/renderer/platform'
function getStateForChannel<T>(
channel: StateChannel,
key: string,
): Promise<{ value: T; hasKey: boolean }> {
return ipcRenderer.invoke(`${channel}:getState`, key)
}
function setStateForChannel<T>(
channel: StateChannel,
key: string,
value: T | undefined,
source: string | undefined,
): Promise<void> {
return ipcRenderer.invoke(`${channel}:setState`, key, value, source)
}
function subscribeToKey<T>(
channel: StateChannel,
key: string,
updatedValue: (\_: any, \_value: T) => void,
): () => void {
ipcRenderer.addListener(
`${channel}:updatedValue:${key}`,
updatedValue
)
ipcRenderer.send(`${channel}:watchKey`, key)
getStateForChannel(channel, key).then((result) => {
updatedValue(key, result.value as T)
})
return () => {
ipcRenderer.removeListener(
`${channel}:updatedValue:${key}`,
updatedValue
)
ipcRenderer.send(`${channel}:unwatchKey`, key)
}
}
const rendererPlatform: Platform = {
setStateForChannel,
subscribeToKey,
getStateForChannel,
}
declare global {
interface Window {
// eslint-disable-next-line @typescript-eslint/ban-types
electronAPI: {
rendererPlatform: Platform
}
}
}
contextBridge.exposeInMainWorld('electronAPI', {
rendererPlatform,
})
In order for our Electron renderer hooks and methods to work we need to first initialize the platform we exposed in the previous step.
Put this code anywhere in your renderer entry point (e.g. renderer.ts
):
import { initializePlatform } from 'electron-shared-state-react/dist/renderer'
initializePlatform(window.electronAPI.rendererPlatform)
You can now use the settings or global state similar to a useState
hook in your application:
import { useGlobalState } from 'electron-shared-state-react/dist/renderer/useGlobalState'
export function NumberIncrementer() {
const [numberOfTasks, setNumberOfTasks] = useGlobalState('numberOfTasks', 0)
return (
<div>
<button onClick={() => setNumberOfTasks(numberOfTasks + 1)}>
Add Task
</button>
<div>{numberOfTasks} Tasks</div>
</div>
)
}
For settings use:
import {
fetchSettings,
setSetting,
} from 'electron-shared-state-react/dist/renderer/useSettings'
// fetch a setting
fetchSettings<string[]>('test').then((settings) => {
console.log(settings)
})
// set a setting
await setSetting('test', ['test'])
For global state use:
import {
fetchGlobalState,
setGlobalState,
} from 'electron-shared-state-react/dist/renderer/useGlobalState'
// fetching a global state
fetchGlobalState<string[]>('test').then((test) => {
console.log(test)
})
// setting a global state
await setGlobalState('test', ['test'])