DEV Community

Cover image for Devlog #3 --- exchange data between Electron and React
Dmytro
Dmytro

Posted on

Devlog #3 --- exchange data between Electron and React

Hello everyone!

Today, I want to talk about the relationship between Electron and React. This is a very important topic, because you usually exchange data between them.

I was using React in Electron Forge, and for my extension, I need to get data from the system and also send a signal to replay the animation.


How to work with data exchange between Electron and React?

So, they exchange data with the help of IPC (Inter-process Communication). They have two types of modules, "ipcMain" and "ipcRender". The first module is used in the main file (index.ts). The second one is used in "preload.ts". To make a connection between them, a channel is used, which you can name whatever you want. This communication can analogous to a Socket, which is also used to channel/event to help exchange data.


How to make it in code?

Well, the first step is to choose a communication pattern. The official site proposes 4 patterns. I consider a pattern called "Main to renderer". Other patterns you can read at this link.

So, let's consider my code in the file "index.ts", which I implemented to send data about MEM, CPU, and Disk.

const sendData = () => {
  for (const [key, value] of Object.entries(funcs)) {
    mainWindow.webContents.send(key, value());
  }
}

const createWindow = (): void => {
  mainWindow = new BrowserWindow({
    height: 200,
    width: 400,
    frame: false,
    transparent: true,
    show: false,
    resizable: false,
    alwaysOnTop: true,
    skipTaskbar: true,
    roundedCorners: true,
    webPreferences: {
      preload: MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY,
    },
  });

  mainWindow.on('blur', () => {
    mainWindow?.hide();
  });

  mainWindow.webContents.on('before-input-event', (event, input) => {
    if (input.key === 'Escape') {
      mainWindow?.hide();
      event.preventDefault();
    }
  });

  mainWindow.loadURL(MAIN_WINDOW_WEBPACK_ENTRY);
  setInterval(sendData, 1000)
};

app.whenReady().then(() => {
  tray = new Tray(icon)

  setInterval(() => {
    const data = MyNativeAddon.watchTray();
    console.log(data);
  }, 1000)

  tray.on("click", () => {
    if (!mainWindow) {
      createWindow();
    }

    if (!position) {
      const { x, y } = positioner.calculate(mainWindow.getBounds(), tray.getBounds())
      position = { x, y }
    }

    if (mainWindow?.isVisible()) {
      mainWindow.hide();
      mainWindow.webContents.send('stop-animation');
    } else {
      mainWindow.setAlwaysOnTop(true, "pop-up-menu");
      mainWindow.setPosition(position.x, position.y + 5);
      mainWindow.show();
      mainWindow.focus();
      mainWindow.webContents.send('start-animation');
    }
  });
})
Enter fullscreen mode Exit fullscreen mode

I ask you to notice this part of the code:

 mainWindow.webContents.send(key, value());
Enter fullscreen mode Exit fullscreen mode

Here, we create a channel on the main side using "ipcMain". Where key is the channel name, after that a value - it's a callback function. The value is not a required parameter.

Then, we need to create the same channel in "preload.ts":

import { contextBridge, ipcRenderer } from "electron";
import funcs from "./objects/funcs.object";

type Callback = (data: any) => void;

const exposedFuncs: Record<
  string,
  (callback: Callback) => void
> = {};

for (const [key] of Object.entries(funcs)) {
  exposedFuncs[key] = (callback: Callback) => {
    ipcRenderer.on(key, (_event, data) => callback(data));
  };
}

contextBridge.exposeInMainWorld('bridge', {
  ...exposedFuncs,
    startAnimation: (cb: () => void) => {
    ipcRenderer.on("start-animation", cb);
  },
  stopAnimation: (cb: () => void) => {
    ipcRenderer.on("stop-animation", cb);
  }
});
Enter fullscreen mode Exit fullscreen mode

So, here we use contextBridge, which uses the exposeInMainWorld method and is written with a similar name as in "ipcMain" and similar key and callback.

Also, if you use TypeScript, you must create a <file_name>.d.ts file and write the following code:

export { };

declare global {
  interface Window {
    bridge: {
      getMemory: GetMemory;
      getCPU: GetCPU;
      getDisk: GetDisk;
    };
  }
}

Enter fullscreen mode Exit fullscreen mode

How to call methods in React from Electron?

After that, when we create a "bridge", we can call methods in React from Electron:

import { useState, useEffect } from "react";

const useSystem = () => {
  const [memory, setMemory] = useState(null);
  const [cpu, setCpu] = useState(null);
  const [disk, setDisk] = useState(null);

  useEffect(() => {
    window.bridge.getMemory(data => setMemory(data));
    window.bridge.getCPU(data => setCpu(data));
    window.bridge.getDisk(data => setDisk(data));
  }, [memory, cpu, disk]);

  return { memory, cpu, disk };
}

export default useSystem

---------------------------------------------------------------------------------

import { useState, useEffect } from "react"

const useVisible = () => {
  const [visible, setVisible] = useState(false)

  useEffect(() => {
    window.bridge.startAnimation(() => setVisible(true))
    window.bridge.stopAnimation(() => setVisible(false))
  }, [])

  return visible
}

export default useVisible
Enter fullscreen mode Exit fullscreen mode

In my case, I used custom hooks for more comfort. But you can notice that I call functions via window... We can say that we expand the Window API which calls Renderer (our the window..) -> Preload -> Main -> Renderer.

In general, today we considered how to create signals between Electron and React in my example code. I hope this devlog helps other beginner developers.

Thanks to all for your attention! I hope you enjoyed reading my blog!

Have a good day!

Additionally, you can read about it on the official Electron website:
https://www.electronjs.org/docs/latest/api/web-contents
https://www.electronjs.org/docs/latest/api/ipc-renderer

Top comments (0)