---
title:
  "Creating a synchronized store between main and renderer process in Electron"
description:
  "Creating a synchronized store between main and renderer process in Electron"
canonical_url: "https://www.bigbinary.com/blog/sync-store-main-renderer-electron"
markdown_url: "https://www.bigbinary.com/blog/sync-store-main-renderer-electron.md"
---

# Creating a synchronized store between main and renderer process in Electron

Creating a synchronized store between main and renderer process in Electron

- Author: Farhan CK
- Published: October 1, 2024
- Categories: JavaScript, ReactJS, Electron

_Recently, we built [NeetoRecord](https://neetorecord.com/neetorecord/), a loom
alternative. The desktop application was built using Electron. In a series of
blogs, we capture how we built the desktop application and the challenges we ran
into. This blog is part 1 of the blog series. You can also read
[part 2](https://www.bigbinary.com/blog/publish-electron-application),
[part 3](https://www.bigbinary.com/blog/video-background-removal),
[part 4](https://www.bigbinary.com/blog/electron-multiple-browser-windows),
[part 5](https://www.bigbinary.com/blog/code-sign-notorize-mac-desktop-app),
[part 6](https://www.bigbinary.com/blog/deep-link-electron-app),
[part 7](https://www.bigbinary.com/blog/request-camera-micophone-permission-electron)
[part 8](https://www.bigbinary.com/blog/native-modules-electron) and
[part 9](https://www.bigbinary.com/blog/ev-code-sign-windows-application-ssl-com)
._

When building desktop applications with [Electron](https://electronjs.org/), one
of the key challenges developers often face is managing the shared state between
the `main` process and multiple `renderer` processes. While the `main` process
handles the core application logic, `renderer` processes are responsible for the
user interface. However, they often need access to the same data, like user
preferences, application state, or session information.

Electron does not natively provide a way to persist data, let alone give a
synchronized state across these processes.

### electron-store to store data persistently

Since Electron doesn't have a built-in way to persist data, We can use
[electron-store](https://github.com/sindresorhus/electron-store), an npm package
to store data persistently. `electron-store` stores the data in a JSON file
named `config.json` in `app.getPath('userData')`.

Even though we can configure `electron-store` to be made directly available in
the `renderer` process, it is recommended not to do so. The best way is to
expose it via
[Electron's preload script](https://www.electronjs.org/docs/latest/tutorial/tutorial-preload).

Let's look at how we can expose the `electron store` to the renderer via a
preload script.

```js
// preload.js
import { contextBridge, ipcRenderer } from "electron";

const electronHandler = {
  store: {
    get(key) {
      return ipcRenderer.sendSync("get-store", key);
    },
    set(property, val) {
      ipcRenderer.send("set-store", property, val);
    },
  },
  // ...others code
};
contextBridge.exposeInMainWorld("electron", electronHandler);
```

Here, we exposed a `set` function that calls the `ipcRenderer.send` method,
which just sends a message to the `main` process. The `get` function calls the
`ipcRenderer.sendSync` method, which will send a message to the `main` process
while expecting a return value.

Now, let's add `ipcMain` events to handle these requests in the `main` process.

```js
import Store from "electron-store";

const store = new Store();

ipcMain.on("get-store", async (event, val) => {
  event.returnValue = store.get(val);
});
ipcMain.on("set-store", async (_, key, val) => {
  store.set(key, val);
});
```

In the `main` process, we created an `electron-store` instance and added
`get-store` and `set-store` event handlers to retrieve and set data from the
store.

Now, we can read and write data from any `renderer` process without exposing the
whole `electron-store` class to it.

```js
window.electron.store.set("key", "value");
window.electron.store.get("key");
```

## Synchronization

Since we sorted out the storage issue, let's look into how we can synchronize
data between the `main` process and all its `renderer` processes.

Before we start synchronization, let's create a simple utility function that can
send a message to all active `renderer` processes or, in other words, browser
windows (we will use the terms `renderer` process and browser windows
interchangeably).

```js
export const sendToAll = (channel, msg) => {
  BrowserWindow.getAllWindows().forEach(browseWindow => {
    browseWindow.webContents.send(channel, msg);
  });
};
```

`BrowserWindow.getAllWindows()` returns all active browser windows, and
`browseWindow.webContents.send` is the standard way of sending a message from
`main` to a `renderer` process.

### electron-store onDidChange

`electron-store` provides an option to add an event listener when there is a
change in the store called `onDidChange`. This is the key feature we are going
to use to create synchronization.

```js
store.onDidChange("key", newValue => {
  // TODO
});
```

Not all data needs to be synchronized. So, instead of adding `onDidChange` to
every field, let's expose an API for the `renderer` process so that it can
decide which data it needs and subscribe to it.

```js {4, 13-18}
import Store from "electron-store";

const store = new Store();
const subscriptions = new Map();

ipcMain.on("get-store", async (event, val) => {
  event.returnValue = store.get(val);
});
ipcMain.on("set-store", async (_, key, val) => {
  store.set(key, val);
});

ipcMain.on("subscribe-store", async (event, key) => {
  const unsubscribeFn = store.onDidChange(key, newValue => {
    sendToAll(`onChange:${key}`, newValue);
  });
  subscriptions.set(key, unsubscribeFn);
});
```

Here, we exposed another API called `subscribe-store`. When calling that API
with a key, we listen to that field's `onDidChange` event. Then, when the
`onDidChange` triggers, we call the `sendToAll` function we created earlier, and
all the `renderer` processes listening to these changes will be notified with
the latest data. For example, if a field called `user` is subscribed to changes,
we send a message to all `renderer` processes with the new value on a channel
called `onChange:user.` We will soon add code in the `renderer` process to
handle this.

`store.onDidChange` returns the `unsubscribe` function for that particular key.
Since we won't be unsubscribing straight away, we need to store this function
for later use. Here, we are storing it in a hash map against the same key.

Let's add an option to unsubscribe as well.

```js
//... other codes

ipcMain.on("unsubscribe-store", async (event, key) => {
  subscriptions.get(key)();
});
```

### Update preload script

Let's update the preload script to support the store's
subscription/unsubscribing.

```js {11-23}
// preload.js
import { contextBridge, ipcRenderer } from "electron";

const electronHandler = {
  store: {
    get(key) {
      return ipcRenderer.sendSync("get-store", key);
    },
    set(property, val) {
      ipcRenderer.send("set-store", property, val);
    },
    subscribe(key, func) {
      ipcRenderer.send("subscribe-store", key);
      const subscription = (_event, ...args) => func(...args);
      const channelName = `onChange:${key}`;
      ipcRenderer.on(channelName, subscription);

      return () => {
        ipcRenderer.removeListener(channelName, subscription);
      };
    },
    unsubscribe(key) {
      ipcRenderer.send("unsubscribe-store", key);
    },
  },
  // ...others code
};
contextBridge.exposeInMainWorld("electron", electronHandler);
```

We add two APIs here, `subscribe` and `unsubscribe`. While `unsubscribe` is
straightforward, `subscribe` might need some explanation. It exposes two
arguments, a store key and a callback function, to be called when there is a
change to that field.

First, we call `subscribe-store` to subscribe to change to that data field;
then, we listen to `ipcRenderer.on` for any changes. For example, when there is
a change to the `user` field, `sendToAll` will propagate the change, and here we
are listening to it on `onChange:user`.

Now, from a `renderer` process, if it needs to be notified of changes to the
`user` field, we can subscribe to it like below.

```js
window.electron.store.subscribe("user", newUser => {
  // TODO
});
```

### useSyncExternalStore

React provides a hook to connect to an external store called
`useSyncExternalStore`. It expects two functions as arguments.

- The `subscribe` function should subscribe to the store and return an
  unsubscribe function.
- The `getSnapshot` function should read a snapshot of the data from the store.

In the `renderer` process, create a `SyncedStore` class with `subscribe` and
`getSnapshot` functions that `useSyncExternalStore` expects.

```js
class SyncedStore {
  snapshot;
  defaultValue;
  storageKey;
  constructor(defaultValue = "", storageKey) {
    this.defaultValue = defaultValue;
    this.snapshot = window.electron.store.get(storageKey) ?? defaultValue;
    this.storageKey = storageKey;
  }
  getSnapshot = () => this.snapshot;

  subscribe = callback => {
    // TODO
  };
}
```

Here, we created a generic class that takes a `defaultValue` and `storageKey`.
While creating the object, we loaded the existing data for that field from the
`main` store.

When React tries to subscribe to this using `useSyncExternalStore`, we need to
call our `main` store's subscribe.

```js {13-16}
class SyncedStore {
  snapshot;
  defaultValue;
  storageKey;
  constructor(defaultValue = "", storageKey) {
    this.defaultValue = defaultValue;
    this.snapshot = window.electron.store.get(storageKey) ?? defaultValue;
    this.storageKey = storageKey;
  }
  getSnapshot = () => this.snapshot;

  subscribe = callback => {
    window.electron.store.subscribe(this.storageKey, callback);
    return () => {
      window.electron.store.unsubscribe(this.storageKey);
    };
  };
}
```

We have our `SyncedStore` ready, but it's a bit inefficient; for example, if we
are subscribed to the same `storageKey` in multiple places, it will create a
subscription for each instance in the main store. That is needless IPC
communications for the same data.

Let's improve this a bit so that only one subscription is registered per browser
window(`renderer` process), and if there are multiple use cases of the same,
let's handle it internally.

```js {5, 13-17, 20-28}
class SyncedStore {
  snapshot;
  defaultValue;
  storageKey;
  listeners = new Set();
  constructor(defaultValue = "", storageKey) {
    this.defaultValue = defaultValue;
    this.snapshot = window.electron.store.get(storageKey) ?? defaultValue;
    this.storageKey = storageKey;
  }
  getSnapshot = () => this.snapshot;

  onChange = newValue => {
    if (JSON.stringify(newValue) === JSON.stringify(this.snapshot)) return;
    this.snapshot = newValue ?? this.defaultValue;
    this.listeners.forEach(listener => listener());
  };

  subscribe = callback => {
    this.listeners.add(callback);
    if (this.listeners.size === 1) {
      window.electron.store.subscribe(this.storageKey, this.onChange);
    }
    return () => {
      this.listeners.delete(callback);
      if (this.listeners.size !== 0) return;
      window.electron.store.unsubscribe(this.storageKey);
    };
  };
}
```

We made the change so that only one request is sent to `main`; the rest of the
subscriptions are stored internally and respond to it when the first one is
notified.

We also added additional checks to ensure that rerender is not triggered if
there are no changes to the data.

### Usage

Now, whenever a synchronized store for a field is needed, we just need to create
an instance of this class and pass it to `useSyncExternalStore`.

```js
import { useSyncExternalStore } from "react";

const createSyncedStore = ({ defaultValue, storageKey }) => {
  const store = new SyncedStore(defaultValue, storageKey);
  return () => useSyncExternalStore(store.subscribe, store.getSnapshot);
};

const useUser = createSyncedStore({
  storageKey: "user",
  defaultValue: { firstName: "Oliver", lastName: "Smith" },
});

const App = () => {
  const user = useUser();

  return <div>Name: {`${user.firstName} ${user.lastName}`}</div>;
};
```

Now, if we update the `user` field from anywhere, let it be from any `renderer`
process or `main`.

```js
window.electron.store.set("user", { firstName: "John", lastName: "Smith" });
```

The above `App` component will be rerendered with the latest user data.

## Links

- [Human page](https://www.bigbinary.com/blog/sync-store-main-renderer-electron)
