Loading... Search articles

Search for articles

Sorry, but we couldn't find any matches...

But perhaps we can interest you in one of our more popular articles?
Learn how to create a desktop app with React and Electron and build it with Codemagic

Building Electron desktop apps with React using Codemagic

Mar 17, 2022

TL;DR: You can use Codemagic CI/CD to build and publish Electron desktop apps. In this article, we will create a sample app for monitoring and trading stocks using React and Electron and build it for macOS, Windows, and Linux using Codemagic.

Build a stocks application with React and Electron

Building a desktop application usually requires a lot of work. You have to learn the language, tools, and processes of each platform you are targeting. For example, if you want to create the application on Windows, you’ll use Visual Studio, and your desktop application will only be available for Windows users.

How do you create the same application on Linux or macOS? Well, you’ll have to learn the languages of these platforms and build the same application separately for each. Conclusion: You’ll need to have three different codebases for the same application.

But with some knowledge of React and JavaScript, you can easily build cross-platform applications. In this article, we’ll build a stocks application with React and Electron. We’ll also create a basic CI/CD pipeline for our Electron desktop app so that we can build it for all the main desktop platforms — Windows, Linux, and macOS.

What is Electron?

Electron is a cross-platform desktop application framework. It is used to build desktop applications with the JavaScript language. It’s definitely also possible to use JavaScript frameworks like React and Vue to build desktop applications with Electron.

A few notes about Electron’s architecture: Electron is a platform-agnostic framework, meaning it’s not tied to any specific platform, language, framework, or tool.

Electron embeds Chromium and Node.js in its core, enabling web developers to write desktop applications using JavaScript and HTML. To do this, Electron implements a multi-process model composed of the main and renderer processes, similar to the Chromium browser.

Each app’s window is a renderer process, which isolates the code execution at the window level. The main process — powered by Node.js — is responsible for not only application lifecycle management, window management, and the renderer process but also access to native APIs, such as system menus, notifications, and tray icons.

Now that we know what Electron is, let’s set up the environment for the project we’ll be building.

You can also build cross-platform desktop apps with Flutter. Check out the article that compares Flutter desktop vs Electron. And check out this post to learn more about building Flutter desktop apps.

Prerequisites

To follow along comfortably with the following tutorial, you will need to have:

  • A basic understanding of React and how it works.
  • Node.js installed. If you don’t already have it, you can install it from here.
  • An API key from Alpha Vantage, a stock API service. If you don’t have one, you can get one from here.

Setting up the project

This article uses create-react-app to set up the project.

npx create-react-app stock-app

A new directory named stock-app will be created. Let’s cd into it.

cd stock-app

Now, let’s install electron and electron-is-dev. electron-is-dev is a package that helps us to run the application in development mode. It checks if the Electron application is running in development mode or production mode.

yarn add electron electron-is-dev

Next, create the configuration file for Electron. This file will be located in the project’s public directory.

// ./public/electron.js
const path = require('path');

const { app, BrowserWindow } = require('electron');
const isDev = require('electron-is-dev');

function createWindow() {
  // Create the browser window.
  const win = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      nodeIntegration: true,
    },
  });

  // and load the index.html of the app.
  // win.loadFile("index.html");
  win.loadURL(
    isDev
      ? 'http://localhost:3000'
      : `file://${path.join(__dirname, '../build/index.html')}`
  );
  // Open the DevTools.
  if (isDev) {
    win.webContents.openDevTools({ mode: 'detach' });
  }
}

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.whenReady().then(createWindow);

// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bars to stay active until the user quits
// explicitly with Cmd + Q.
app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit();
  }
});

app.on('activate', () => {
  if (BrowserWindow.getAllWindows().length === 0) {
    createWindow();
  }
});

Let’s quickly explain what we’ve done here:

  • app is the Electron application.
  • BrowserWindow is the class that represents the browser window. It’s used to render web content. It also handles the events of the window, like closing, resizing, etc.
  • isDev is a package that helps us to run the application in development mode. It checks if the Electron application is running in development mode or production mode.
  • app.whenReady() is a function that returns a promise. It’s used to wait until the application is ready to create the browser window.
  • app.on('window-all-closed', () => { is a function that is called when all the windows are closed.
  • if (process.platform !== 'darwin') { is a condition that checks if the platform is not macOS.
  • app.quit(); is a function that quits the application.

You can learn more about Electron configuration here.

The significant change is that we have added a custom index.html file to be launched. This will be in your build file, which will be the destination in production.

The final step for the configuration is to rewrite the package.json file. This is important because we need to tell Electron where to find the main file and how to launch the desktop application.

Before doing so, install concurrently and wait-on. You can use these packages to run the application in development mode. When you try to launch the application in the browser, the browser will launch an Electron application instead.

yarn add concurrently wait-on -D

Concurrently allows us to run multiple commands within one script. wait-on will wait for port 3000, the default CRA port, to launch the app.

Open your package.json file and add this entry.

"main": "public/electron.js",
"homepage": "./"

In the scripts section, add the following:

...
"scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject",
    "dev": "concurrently -k \"BROWSER=none yarn start\" \"yarn:electron\"",
    "electron": "wait-on tcp:3000 && electron ."
  },

Great! We have installed everything. Now you can launch the application with yarn dev.

Developing the application

Now that everything is set up, let’s build our stocks application. To fetch the data from the API, we’ll use the axios and swr packages. To display a candlestick chart, we’ll use the apexcharts package.

yarn add axios swr apexcharts react-apexcharts

Great! Now, let’s build the application.

In the src directory, create a new file named config.js. This file will contain the configuration for the application.

export const API_URL = "https://www.alphavantage.co";

export const symbols = ['MSFT', 'AAPL', 'AMZN', 'FB', 'GOOG'];

After that, create another file named axios.js. This file will contain the fetch method that will be used for the hooks we’ll be writing to get data from the Alpha Vantage API.

import axios from "axios";
import { API_URL } from "./api";

const axiosService = axios.create({
    baseURL: API_URL
});

export function fetcher(url, params) {
    return axiosService.get(`${url}${params}&function=TIME_SERIES_DAILY&outputsize=compact&apikey=${process.env.REACT_APP_API_KEY}`).then((res) => res.data);
}

export default axiosService;

As you can see here, the fetcher function is used to fetch data from the API. The baseURL is the base URL of the API. The url is the path of the API. The params are the parameters that will be sent to the API. We need to create the REACT_APP_API_KEY variable in an .env file.

Securing the sensitive data

React provides a way to secure the data that is sent to the API. We’ll just create an .env file and add the REACT_APP_API_KEY variable. When working with React and environment variables, make sure all your environment variables start with REACT_APP_.

Create an .env file at the root of the project — and make sure to add it to .gitignore, as it will be storing our secrets. Alternatively, you can use .env.local for this, as it is already in .gitignore. Add this to the file:

REACT_APP_API_KEY=<your-api-key>

Great! Now that we have the fetcher function, let’s create a useAlphaVantage hook.

Writing the useAlphaVantage hook

A React hook is a function that returns a value. Why use a hook? Hooks make React so much better because they enable you to use simpler code that implements similar functionalities faster and more effectively. You can also implement React state and lifecycle methods without writing classes.

What’s even cooler is that we can create our own hooks. Inside the src directory, create a new file named useAlphaVantage.js.

In this file, add the following code:

import useSWR from "swr";
import { fetcher } from "./axios";

const useAlphaVantage = (params) => {

    const seriesData = useSWR(
        ["/query", params],
        fetcher,
        {
            refreshInterval: 20000
        }
    );

    if (seriesData.error) return null;

    if (seriesData.data) {
        const data = seriesData.data["Time Series (Daily)"];

        if (!data) return null;

        const series = Object.keys(data).map(key => {

            const values = Object.values(data[key]);
            values.pop();
            return {
                x: key,
                y: values
            };
        });
        
        return series;
    }

}

export default useAlphaVantage;

What are we doing here? Let’s explain:

  • useSWR is a hook that allows us to fetch data from the API. It needs to know the URL of the API and the function that will be used to fetch the data. We have also added a refresh interval of 20 seconds.
  • fetcher is the function that we’ll use to fetch the data.
  • seriesData is the data that will be returned by the hook.
  • if (seriesData.error) return null; is a condition that checks if there is an error. If there is an error, we return null.
  • if (seriesData.data) is a condition that checks if there is data. If there is data, we can work with it and return exactly what we want.

The hook accepts a params parameter. This parameter will basically be the symbol that we’ll be using to fetch the data. If this symbol changes, the hook will automatically fetch the data again.

Great! The hook is now implemented. Let’s add it to the App.js application.

Writing the UI logic

ApexCharts is a data visualization library that allows us to display charts. Since it’s well integrated with React, we can use it to display the candlestick chart.

The ApexCharts documentation is available here.

It comes with a component called Chart that we can use by passing the necessary props.

import { useState } from 'react';

import Chart from 'react-apexcharts'
import useAlphaVantage from './useAlphaVantage';

import { symbols } from './api';

import './App.css';

function App() {


  const [symbol, setSymbol] = useState("MSFT");

  const dataAlpha = useAlphaVantage(`?symbol=${symbol}`);

  if (!dataAlpha) return <div className='loading'>Loading...</div>;

  const state = {
    series: [{
      data: dataAlpha
    }],
    options: {
      chart: {
        type: 'candlestick',
        height: 350
      },
      title: {
        text: 'Candlestick Chart',
        align: 'left'
      },
      xaxis: {
        type: 'datetime'
      },
      yaxis: {
        tooltip: {
          enabled: true
        }
      }
    },  
  };

  return (
    <div>
    </div>
  );
}

export default App;

We can also add the UI.

return (
    <div>
      <div>
        <select className="select" value={symbol} onChange={e => {setSymbol(e.target.value)}}>
          {symbols.map(symbol => <option key={symbol} value={symbol}>{symbol}</option>)}
        </select>
      </div>
      <Chart options={state.options} series={state.series} type="candlestick" height={350} />
    </div>
  );

A select menu will be used to select the symbol.

Let’s add some CSS to make the application look nice. Inside the App.css file, add the following code:

.select {
  width: 100%;
  height: 40px;
  border-radius: 4px;
  border: 1px solid #ccc;
  padding: 0 10px;
  font-size: 16px;
  color: #333;
  outline: none;
}

.loading {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(255, 255, 255, 0.8);
  z-index: 10;
  display: flex;
  justify-content: center;
  align-items: center;
}

Once that’s implemented, you’ll have a UI that looks like this:

Application UI

Excellent! Now that we have a working application, let’s see how we can build it for different platforms.

Building the Electron app

We have a working Electron application, and we can build it on different desktop platforms. There are many tools that we can use to build the application, the main three being electron-builder, electron-forge, and electron-packager.

In this tutorial, we will use electron-forge to build our stocks app for macOS, Windows, and Linux.

yarn add electron-forge

For macOS, add this dev dependency:

yarn add -D  @electron-forge/maker-dmg

Once that’s done, let’s add some requirements to the package.json file. Make sure that the scripts section looks like this.

...
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject",
    "dev": "concurrently -k \"BROWSER=none yarn start\" \"yarn:electron\"",
    "electron": "wait-on tcp:3000 && electron .",
    "package": "react-scripts build && electron-forge package",
    "make-mac": "react-scripts build && electron-forge make --platform darwin",
    "make-linux": "react-scripts build && electron-forge make --platform linux",
    "make": "react-scripts build && electron-forge make"
  },
...

And add the config section in the package.json file.

"config": {
    "forge": {
      "packagerConfig": {},
      "makers": [
        {
          "name": "@electron-forge/maker-squirrel",
          "config": {
            "name": "stock_trading_app"
          }
        },
        {
          "name": "@electron-forge/maker-zip",
          "platforms": [
            "darwin",
            "linux",
            "win32"
          ]
        },
        {
          "name": "@electron-forge/maker-deb",
          "config": {}
        },
        {
          "name": "@electron-forge/maker-rpm",
          "config": {}
        }
      ]
    }
  }

Here’s the full package.json file.

{
  "name": "stock-trading-app",
  "version": "0.1.0",
  "private": true,
  "license": "MIT",
  "main": "public/electron.js",
  "homepage": "./",
  "author": {
    "name": "Kolawole Mangabo",
    "email": "kolagithub@gmail.com"
  },
  "description": "A simple Electron app",
  "dependencies": {
    "@testing-library/jest-dom": "^5.16.2",
    "@testing-library/react": "^12.1.2",
    "@testing-library/user-event": "^13.5.0",
    "apexcharts": "^3.33.1",
    "axios": "^0.25.0",
    "electron-is-dev": "^2.0.0",
    "electron-squirrel-startup": "^1.0.0",
    "react": "^17.0.2",
    "react-apexcharts": "^1.3.9",
    "react-dom": "^17.0.2",
    "react-scripts": "5.0.0",
    "swr": "^1.2.1",  
    "web-vitals": "^2.1.4"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject",
    "dev": "concurrently -k \"BROWSER=none yarn start\" \"yarn:electron\"",
    "electron": "wait-on tcp:3000 && electron .",
    "package": "react-scripts build && electron-forge package",
    "make-mac": "react-scripts build && electron-forge make --platform darwin",
    "make-linux": "react-scripts build && electron-forge make --platform linux",
    "make": "react-scripts build && electron-forge make"
  },
  "eslintConfig": {
    "extends": [
      "react-app",
      "react-app/jest"
    ]
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  },
  "devDependencies": {
    "@electron-forge/cli": "^6.0.0-beta.63",
    "@electron-forge/maker-deb": "^6.0.0-beta.63",
    "@electron-forge/maker-dmg": "^6.0.0-beta.63",
    "@electron-forge/maker-rpm": "^6.0.0-beta.63",
    "@electron-forge/maker-squirrel": "^6.0.0-beta.63",
    "@electron-forge/maker-zip": "^6.0.0-beta.63",
    "concurrently": "^7.0.0",
    "electron": "^17.0.0",
    "wait-on": "^6.0.0"
  },
  "config": {
    "forge": {
      "packagerConfig": {},
      "makers": [
        {
          "name": "@electron-forge/maker-squirrel",
          "config": {
            "name": "stock_trading_app"
          }
        },
        {
          "name": "@electron-forge/maker-deb",
          "config": {
            "name": "stock_trading_app"
          }
        },
        {
          "name": "@electron-forge/maker-rpm",
          "config": {
            "name": "stock_trading_app"
          }
        },
        {
          "name": "@electron-forge/maker-dmg",
          "config": {
            "name": "stock_trading_app",
            "format": "ULFO"
          }
        },
        {
          "name": "@electron-forge/maker-zip",
          "platforms": [
            "darwin",
            "linux",
            "win32"
          ]
        }
      ]
    }
  }
}

Now that we have configured our Electron app to be built for all three major desktop operating systems, we can move to Codemagic and start writing the building script for the CI/CD pipeline.

CI/CD for Electron apps with Codemagic

Codemagic helps you write CI/CD pipelines for mobile or desktop applications, including building, testing, and publishing your apps. You can use it for Electron apps as well.

In this tutorial, we will first write a build script targeting Linux machines. Then, we’ll add the scripts to build the desktop application on Windows and macOS.

First, make sure you have created a Codemagic account. If you haven’t, sign up here.

Sign up

Once that’s done, add a new application. Codemagic’s teams feature provides a way to not only better manage your build scripts but also share many instances, which is very helpful if you are looking to build your application on multiple machines.

Selecting teams

After that, you’ll need to specify the type of application. In this case, we’ll go with Other and enter Electron App as the name. And don’t forget to select the application from your preferred repository.

Type of application

Now, we can start writing the script. Create a file called codemagic.yaml at the root of your project.

Build the Electron app for Linux

Inside the codemagic.yaml file, add the following content.

workflows:
  linux-build:
    name: Linux Build
    instance_type: linux
    environment:
      groups:
        - prod    
      node: 16.14.0
    
    scripts: 
      - name: Injecting env vars
        script: echo "REACT_APP_API_KEY=$REACT_APP_API_KEY" >> .env
      - name: Installing packages
        script: yarn install
      - name: Installing RPM to build for arch linux
        script: sudo apt install rpm -y
      - name: Building Applications
        script: yarn make-linux

    artifacts:
      - out/make/deb/x64/*.deb
      - out/make/rpm/x64/*.rpm
      - out/make/zip/linux/x64/*.zip

Here, we are building our Electron application for Debian and Arch Linux using a standard Linux machine that Codemagic provides. We’ll get .deb and .rpm files as build artifacts that we can run on the machines with their respective operating systems. It is also possible to make snaps of your Linux apps and publish them to the Snap Store with Codemagic — you can read more about this here.

Build the Electron app for macOS

Building the Electron application on macOS — and for macOS — is very similar to building it on Linux.

Add the following workflows to the codemagic.yaml file.

  macos-build:
    name: macOS Build
    instance_type: mac_mini
    environment:
      groups:
        - prod 
      node: 16.14.0

    scripts:
      - name: Injecting env vars
        script: echo "REACT_APP_API_KEY=$REACT_APP_API_KEY" >> .env
      - name: Installing packages
        script: yarn install
      - name: Building Applications
        script: yarn make-mac

    artifacts:
      - out/make/*.dmg

In this example, we will get an unsigned .dmg file that will contain our Electron app. If you want to learn how to code sign it and publish it to the App Store, take a look at the docs.

Finally, we can add the scripts to build the desktop application on Windows.

Build the Electron app for Windows

We can use a very similar workflow for Windows as for the other platforms.

  windows-build:
    name: Windows Build
    instance_type: windows_x2
    environment:
      groups:
        - prod   
      node: 16.14.0
  
    scripts:
      - name: Injecting env vars
        script: echo "REACT_APP_API_KEY=$REACT_APP_API_KEY" >> .env
      - name: Installing packages
        script: yarn install
      - name: Building Applications
        script: yarn run make

    artifacts:
      - out/make/squirrel.windows/x64/*exe
      - out/make/squirrel.windows/x64/*nupkg
      - out/make/zip/win32/x64/*.zip

Here, we’re using a premium Windows VM from Codemagic to get an .exe artifact.

Now, remember that Codemagic allows you to automate building and publishing for all of your target platforms, so you can put all of these yaml scripts into one codemagic.yaml file. As a result, it will look like this:

workflows:
  linux-build:
    name: Linux Build
    instance_type: linux
    environment:
      groups:
        - prod    
      node: 16.14.0
    
    scripts: 
      - name: Injecting env vars
        script: echo "REACT_APP_API_KEY=$REACT_APP_API_KEY" >> .env
      - name: Installing packages
        script: yarn install
      - name: Installing RPM to build for arch linux
        script: sudo apt install rpm -y
      - name: Building Applications
        script: yarn make-linux

    artifacts:
      - out/make/deb/x64/*.deb
      - out/make/rpm/x64/*.rpm
      - out/make/zip/linux/x64/*.zip

  windows-build:
    name: Windows Build
    instance_type: windows_x2
    environment:
      groups:
        - prod   
      node: 16.14.0
  
    scripts:
      - name: Injecting env vars
        script: echo "REACT_APP_API_KEY=$REACT_APP_API_KEY" >> .env
      - name: Installing packages
        script: yarn install
      - name: Building Applications
        script: yarn run make

    artifacts:
      - out/make/squirrel.windows/x64/*exe
      - out/make/squirrel.windows/x64/*nupkg
      - out/make/zip/win32/x64/*.zip


  macos-build:
    name: macOS Build
    instance_type: mac_mini
    environment:
      groups:
        - prod 
      node: 16.14.0

    scripts:
      - name: Injecting env vars
        script: echo "REACT_APP_API_KEY=$REACT_APP_API_KEY" >> .env
      - name: Installing packages
        script: yarn install
      - name: Building Applications
        script: yarn make-mac

    artifacts:
      - out/make/*.dmg

Before building the scripts, you’ll need to add an env var. In this case, it’ll be REACT_APP_API_KEY. For each build, we want Codemagic to inject the secret in the .env file.

Adding secret keys

And voilà! We’ve just created an electron application and written CI/CD pipelines to build the application on different platforms.

Conclusion

In this article, we’ve covered how to create a simple Electron app to monitor stocks using React and the Alpha Vantage API, relying on the axios, swr, and apexcharts packages. We’ve also created a simple pipeline with Codemagic CI/CD to build our Electron app for all three major desktop operating systems — macOS, Linux, and Windows.

Being able to deliver cross-platform apps is one of Electron’s main advantages, and Codemagic CI/CD can help you get the most out of it by automatically building them. You can also automate publishing the app to the App Store, Microsoft Store, and Snap Store and add a webhook to trigger builds automatically. But that’s a story for another blog post.

If you want to learn more about Electron CI/CD, tell us by tagging @codemagicio on Twitter or posting to #codemagic-feedback on Codemagic’s Slack. Or use the form below.

Finally, you can find the code for this project with a working codemagic.yaml file on GitHub here.


Kolawole Mangabo is a full-stack engineer who works with Python and JavaScript. Currently busy building things in a foodtech company, he’s also a technical writer who covers Django, React, and React Native on his personal blog.

Related articles

Latest articles

Show more posts