avatarFilip Jerga, September 23, 2020

Electron & React JS: Build a Native App with Javascript, Part 2

Learn how to integrate the React library into your Electron app.

TODO: provide alt

Chapter 0 — Resources

Full Course: Electron & React JS: Build Native Chat App with Javascript

Youtube Guide: https://youtu.be/VCl8li22mrA

Github Repo: https://github.com/Jerga99/electron-react-boilerplate

Part 1: https://codeburst.io/electron-react-js-build-a-native-app-with-javascript-part-1-537111c80df8

Chapter 1 — Let’s talk React JS

In this part of tutorial, you will learn how to integrate React JS into your Electron applications.

I hope you still have your application from the previous part of this tutorial. If not, check the first part of this tutorial. It will take less than 5 min to create the initial application.

Let’s start!

  1. Open your terminals in the projects folder and install React, npm install --save react react-dom
  2. create a new folder in your project /src/js and inside a file called index.js
  3. Mount a JSX content into HTML
import React from 'react';
import ReactDOM from 'react-dom';

import App from './App';
import './index.scss';

ReactDOM
  .render(<App />, document.getElementById('root'));
index.js

Now we need to create index.scss and the App.js component in the same folder.

h1 {
  color: blue;
}
index.scss
import React from 'react';

export default function App() {
  return (
    <>
      <h1>My Electron/React App</h1>
      <button>Notify</button>
    </>
  )
}
App.js

Perfect! A very simple application displaying just h1 and the button. How do we want to load this Javascript into our electron app?

We just need to specify a script loading the entry file which is index.js into the Html.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="Content-Security-Policy" content="script-src 'self'" />
    <title>Electron Chat</title>
  </head>
  <body>
    <div id ="root"></div>
    <script src="./src/js/index.js"></script>
  </body>
</html>
index.html

Not let's try to run your Electron apps - npm start . What do you think? Will it work?

*Developer tools

Unfortunately not. Electron is executing JS in the Node JS environment. Node js is following ECMAScript 2015 (ES6). This allows writing modern JS -> classes, destructuring… but as the module system it’s using Common JS which doesn’t allow imports.

*If you want to open developer tools as on the picture, press command option+cmd+I or on windows control+shift+I or in your application menu toggle developer tools.

You could import a module like this: const module = require("moduleFile.js")

So would it help just to change the imports?

const React = require('react');
const ReactDOM = require('react-dom');
index.js

Well, no. React is using JSX and syntax that cannot be executed normally by the JS engine.

What is the solution?

We can take all of our React JS files and compile them into a version of JS that can be executed in the Electron/Node. For this, we will use Babel/Webpack.

Here is the simple flow: Take as the input React JS files, compile them, and give me a version of JS I can use in index.html

Input: React JS Files -> Compile by: Webpack/Babel -> Output: Compiled React JS -> Place in: index.html

Chapter 2— Webpack & Babel

To have the ability to compile React and Scss files we need to first install some dependencies. Actually, quite a lot.

Let’s run npm install --save-dev @babel/core @babel/preset-env @babel/preset-react babel-loader css-loader style-loader sass-loader sass webpack webpack-cli

You can also find the full list here: https://github.com/Jerga99/electron-react-boilerplate/blob/master/package.json

Now when everything is installed create in the root folder of your projects file with the name: webpack.common.js and add the following content:

const path = require('path');

module.exports = {
  mode: 'development',
  entry: './src/js/index.js',
  devtool: 'inline-source-map',
  target: 'electron-renderer',
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: [[
              '@babel/preset-env', {
                targets: {
                  esmodules: true
                }
              }],
              '@babel/preset-react']
          }
        }
      },
      {
        test: [/\.s[ac]ss$/i, /\.css$/i],
        use: [
          // Creates `style` nodes from JS strings
          'style-loader',
          // Translates CSS into CommonJS
          'css-loader',
          // Compiles Sass to CSS
          'sass-loader',
        ],
      }
    ]
  },
  resolve: {
    extensions: ['.js'],
  },
  output: {
    filename: 'app.js',
    path: path.resolve(__dirname, 'build', 'js'),
  },
};
webpack.common.js

Important to note here is the input which is the path to our root React JS file which is in ./src/js/index.js and output which will be generated in the base folder of your application under /build/js/app.js

Other parts of this file just define how JS/SCSS should be compiled and which loaders should be used for this purpose.

Now we are ready to compile our code.

Open package.json and specify a new script:

"watch": "webpack --config webpack.common.js --watch"
package.json

Run command: npm run watch

In your consoles, you should see the output from Webpack.

Check your build/js/app.js it’s a huge file containing all compiled code from which your React application consists of.

Now we need to provide this file into index.html

<script src="./build/js/app.js"></script>

Now we can run: npm start and our applications will be working again! Integration with React is completed. Now you can write native applications with the help of the React library. Isn’t that awesome?

Whenever you will do changes in the src/js folder changes will be taken by Webpack and new output will be compiled.

However, to see new changes you need to restart your Electron application, that’s not very convenient. We can fix it very easily.

Let’s install: npm install --save-dev electron-reload

And in the main.js file write:

const path = require('path');
const isDev = !app.isPackaged;

if (isDev) {
  require('electron-reload')(__dirname, {
    electron: path.join(__dirname, 'node_modules', '.bin', 'electron')
  })
}
main.js

Now your Electron will be restarted automatically when you will do changes in your React components. Great!

Chapter 3— Communication with the Main Process

The benefit of writing JS in Electron is to have an access to the “lower” API of your operating system. Most of these APIs are defined under Electron and Node modules.

Exposing these APIs to the Renderer process would be considered unsafe but there is a good way to do it more safely.

Let’s say we would like to display native Notification to our application.

First, we need to create a preload script where we will expose some of the functionality from the Main to Rendered process.

Go to main.js and add preload script in webPreferences of BrowserWindow.

webPreferences: {
  nodeIntegration: false,
  worldSafeExecuteJavaScript: true,
  contextIsolation: true,
  preload: path.join(__dirname, 'preload.js')
}
main.js

In the base folder of your projects create preload.js

const { ipcRenderer, contextBridge } = require('electron');

contextBridge.exposeInMainWorld('electron', {
  notificationApi: {
    sendNotification(message) {
      ipcRenderer.send('notify', message);
    }
  }
})
preload.js

Now in the rendered process, we will have an access to electron.notificationApi.sendNotification function. This function will send a message/event of type notify .

Let’s call this function in our React App.js component.

import React from 'react';

export default function App() {
  return (
    <>
      <h1>My Electron/React App</h1>
      <button
        onClick={_ =>
          electron
            .notificationApi
            .sendNotification('Hi there!')}>Notify</button>
    </>
  )
}
App.js

Every time you will click on the notify button a function from the preload script will be executed.

There is just one problem. Nobody listening to this message. We will create a listener in our main.js file. Let’s write in main.js

const { ipcMain, Notification } = require('electron');

ipcMain.on('notify', (_, message) => {
  new Notification({title: 'Notify', body: message}).show();
})
main.js

Now it’s perfect! When you click a button a message will be emitted and code in the main process will be executed. The main process will create a new notification and it will display it on the screen.

Conclusion

That should be it from this part of the tutorial. If Electron is something that interests you then feel free to check my full course:

Full Course: Electron & React JS: Build Native Chat App with Javascript

Youtube Guide: https://youtu.be/VCl8li22mrA

Github Repo: https://github.com/Jerga99/electron-react-boilerplate

Have a nice day!

Cheers,

Filip