Electron + React + Python (Part 3) — Boilerplate 2/3
In the last article we prepared a boiler plate that made use of CRA. The boiler plate that we created could be used to develop a React based UI that could run smoothly in an electron shell. But the main purpose of this boilerplate was to let users take advantage of python scripts for heavy lifting under the hood.
If you want to skip the explanation, you can directly use the updated boilerplate that comes packed with sample code to help you get started.
Keep in mind that this boiler plate does not use my library and has a lot of complexity involved. For production ready code, please wait for my next article.
In this article we would be picking up from where we left off and finish up the boiler plate to support python code.
Note: To begin with the explanation I have created a sample app from the boilerplate that we created in the last article.
A bird’s eye view…
If I were to describe the whole process in one go, it would be:
- A function in the renderer process talks to the main process and asks for a background process (hidden renderer process) to be run.
- The main process has access to APIs that could create a new hidden browser window (hidden renderer process).
- This new hidden browser window will make use of third party library to execute a python script. (Python-Shell is the 3rd party library that we will be using)
- The output of the python script is then conveyed back to the hidden browser window.
- The hidden browser window sends the data back to main process and the main process redirects it back to the original renderer process that initiated the request for the user to see the final output.
Before we process, I urge you to read the following documentation:
Preparing electron.js — main process
The first question that you should be asking is why electron.js and not some other file? — This is the only piece of code that has access to the instance of ipcMain and as you know an app can have no more than one ipcMain instance.
We make use of ipcMain to setup listeners that listen for events being triggered from renderer processes.
So any message from a renderer process (in our case, the React UI) can be sent to the main process.
The following modifications in electron.js would be needed to setup a listener.
ipcMain.on(
'EVENT_LISTENER_NAME',
(event, args) => {
/* ---- Code to execute as callback ---- */
}
);
Note: Explaining electron basics is not the objective of this article, but if you read the docs properly, you know that you can pass around data while triggering an event, and use that data while firing the callback at the listener. This is play a major role in helping us communicate with python scripts.
Note: There is an interesting thing to note about the first argument of the callback in the event listener — event. This argument gives you information about the process that triggered the event, and you can make use of this argument to send a message back to the triggering process. The Electron.js guys have provided really nice examples around this concept.
So for the sake of having an easy example, let us just set up a two event listeners. Why two? If you read the concept I described right at the beginning of this article, I said that the visible renderer process will send a request to main process — for that we will need one event listener. But there is another hidden renderer that will be used to execute the background task and when then hidden renderer wants to relay the processed data back to visible renderer, it will have to do it via the main process as shown in the diagram — for this we will needed the second event listener.
After setting up electron.js, it should look something like this.
Note: You may notice that I have left the callback blank, but that has been done on purpose. We will write the code for it when we revisit electron.js later in this article.
Preparing React UI (app.js) — visible renderer process
Now setup a few things in our UI to make sure we get a confirmation when our test runs successfully. There are two things the visible UI needs to do:
- Request main renderer to start a background process and process the data being provided by the UI.
- Listen for an event that the background process will send to visible renderer via the main renderer process.
So with the above points in mind, this is how we can visualize the app so far
The following snippet (which will be placed in the ReactUI code)would be needed to trigger an event that the event listener, that we just set up in main renderer, would react to.
ipcRenderer.send(
'BACKGROUND_PROCESS_START',
{
"Key1": 123,
"Key2": 456
}
);
So technically speaking, other than a few syntactic differences, everything is just the same for main process and renderer process. To keep things simple, let us trigger the request for background processing right when our UI mounts — componentDidMount
componentDidMount(){
ipcRenderer.on(
'MESSAGE_FROM_BACKGROUND_1',
(event, args) => {
const { message } = args;
console.log(message);
}
); ipcRenderer.send(
'START_BACKGROUND_1',
{
"number": 25
});
}
After making the required changes, your code should look something like this.
Note: Pay close attention to the import statements. Whenever you need access to the ipcRenderer object, you’ll have to import it as shown for every file you access it in.
const electron = window.require('electron');
const { ipcRenderer } = electron;
If you get an error saying window.require is not a function then take a look at this:
Note: So you’ll just need to update a function in electron.js and you should be good to go. To have access to node modules in the renderer process, this change will be mandatory.
Preparing Hidden renderer (background.html) — hidden renderer process
Now for the background process. There are a few things to note about this process:
- It is just another renderer process, nothing special.
- As opposed to the visible renderer process, this will be a hidden browser window. So no for of user interaction will be possible with it.
- As this is yet another window and the user has no way of interacting with it, it will be your responsibility to clean up and close this window else you will end up leaking memory.
All this things are pretty important and handling them every time you need a background process becomes repetitive and bug prone after a while. So I wrote a little library to abstract away these things. But in this tutorial I’ll show you how to do it without the library so that you know what all things the library is actually doing for you.
So let’s break it down into steps:
- When the visible renderer process asks for a background job to be done, the main process will create a new window. (We can dispatch the data we wish to send to the python script only after we are sure that this window has been created successfully).
- The hidden window gets created by the main process.
- After successful initialization, the hidden window fires an event telling the main process that it is ready to accept data.
- The main process sends appropriate data to the hidden process and the hidden process fires up the python script with the data it has been provided.
- The python script sends the processed information back to the hidden renderer, which again makes use of events to relay this information back to visible renderer via main process.
This is what the visualization would look like
Note: We’ll deal with the window creation when we revisit electron.js
Keeping the above steps in mind, let’s start by firing a BACKGROUND_READY event which the main process will listen to.
ipcRenderer.send('BACKGROUND_READY');
We will use the following snippet to setup a listener that will trigger the actual function that will start the python script. This event will be triggered by the main process when it gets a confirmation that the hidden renderer process has been initialized successfully and is now ready to accept data.
ipcRenderer.on('START_PROCESSING',
(event, args) => {
const { data } = args;
let pyshell = new PythonShell(path.join(__dirname,
'/../scripts/factorial.py'),
{pythonPath: 'python3', args: [data]
});
pyshell.on('message',function(results) {
ipcRenderer.send(
'MESSAGE_FROM_BACKGROUND',
{ message: results }
);
});
});
After making the required changes, your code should look something like this.
Revisiting electron.js — Gluing everything together
Now let’s come back to electron.js and make the final changes that we discussed in the above section. We need an event listener to listen to BACKGROUND_READY event and send the data that we will store in a global variable. We also need an event listener setup to listen to the output from the background process and relay it back to visible renderer.
The red arrows are the confirmation messages that are used to give the main process a signal that out hidden renderer is ready and the main process can now send in the data for processing.
The following lines of code will do so:
ipcMain.on('BACKGROUND_READY', (event, args) => {
event.reply('START_PROCESSING', { data: cache.data, });
});
After this, we will put in the code to create the hidden window using the BrowserWindow class as shown in the snippet below. I have few of the logical explanations for the readers to understand on their own. I have heavily commented the whole code but you still get stuck, feel free to ask.
ipcMain.on('START_BACKGROUND_VIA_MAIN',
(event, args) => {
const backgroundFileUrl = url.format(
{ pathname: path.join(__dirname,
`../background_tasks/background.html`),
protocol: 'file:',
slashes: true, }
);
hiddenWindow = new BrowserWindow({ show: false,
webPreferences: { nodeIntegration: true, },
});
hiddenWindow.loadURL(backgroundFileUrl);
hiddenWindow.webContents.openDevTools();
hiddenWindow.on('closed', () => { hiddenWindow = null; });
cache.data = args.number;
}
);
After the changes have been made, this is what electron.js should look like:
Time to add in some python code
Pay close attention to where I am keeping the python scripts.
Edit: Thanks to a vigilant reader, do install python-shell before adding the following code:
npm install --save python-shell
Rule of thumb — Always execute npm install when you clone any node project from Github. 👍
This is how the python script will fit in our diagram.
For the sake of simplicity, the code can literally be anything trivial. Let’s just find the factorial of a given number.
For now I have passed data to the python code via arguments, but there are even more creative and flexible ways of passing data back and forth. More on that in the next article.
Time for a test drive
Now if you have done everything right, you are good to start the app. Go ahead and open a terminal and change your directory into the project root and execute the following command.
npm start
You should see the following output
Congratulations, you have processed 25! through a python script and displayed the data on a browser console.
I ain’t giving you false hope, but the power of this architecture is up to your imagination. Given the fact that this was used for a real time application (will hell lot of optimizations, obviously) I believe it to be a bullet proof framework.
In the upcoming article we will modify package.json further to support production build and we will also take a look at a very convenient library that boils down all the above sorcery into a few lines of code.
If you missed the previous article, do give it a read.