Siegfried Grimbeek Web developer, open source enthusiast, agile evangelist, and tech junkie. Currently hacking away at the frontend for Beerwulf 🙌

Electron alternatives: Exploring NodeGUI and React NodeGUI

13 min read 3836

Electron Alternatives: Exploring NodeGUI and React NodeGUI

Introduction

In this post we will explore the freshly released NodeGUI framework, with the main focus on the React NodeGUI module.

To do this, we are going to develop a system utility monitor application that will work on Linux, Mac, and Windows operating systems.

What is the NodeGUI framework?

Similar to Electron, NodeGUI is an open source library for building cross-platform native desktop applications with JavaScript and CSS-like styling. NodeGUI apps can run on Mac, Windows, and Linux from a single codebase.

What differentiates it from Electron is that it is powered by Qt5, which is excellent for performance and memory, but it does force one to then use their components instead of HTML, as with Electron.

From the NodeGUI release announcement:

NodeGUI is powered by Qt5 💚 which makes it CPU and memory efficient as compared to other chromium based solutions like electron. … NodeGUI wants to incorporate everything that is good about Electron: The ease of development, freedom of styling, Native APIs, great documentation, etc. At the same time NodeGUI aims to be memory and CPU efficient.

React NodeGUI enables developers to build performant native and cross-platform desktop applications with native React and powerful CSS-like styling.

We will make use of the react-node-gui-starter project to bootstrap our application and get up and running quickly.

Prerequisites

To follow along with this tutorial, you will need to have Node installed, an IDE, and a terminal application (I use iTerm2 for Mac and Hyper for Windows).

The application will be built with TypeScript, React, and CSS, so basic knowledge will be handy but is not required as we will walk through every aspect.

System utility monitor application

We will build a simple application that will dynamically display an operating system’s CPU, memory, and disk space, as well as some additional statistics related to the operating system.

With the application, we aim to cover the following:

  • Basics of NodeGUI
  • Basics of React NodeGUI
  • Node core integration with NodeGUI
  • Some caveats of the above

The end result will look as follows:

Final Application On MacOS
Our final application on macOS.

Let’s write some code

As we will be using the react-node-gui-starter project, we can get started by running the following code in your terminal application, which will clone the starter application:

# Clone this repository
git clone https://github.com/nodegui/react-nodegui-starter

# Go into the repository
cd react-nodegui-starter

# Install dependencies
npm install

Additionally, we will need to install one more npm packages that will allow us to access our systems information:

npm i node-os-utils

node-os-utils is an operating system utility library. Some methods are wrappers of Node libraries, and others are calculations made by the module.

Application scripts and development

The starter application offers a few npm scripts that we can run:

"build": "webpack -p",
"start": "webpack && qode ./dist/index.js",
"debug": "webpack && qode --inspect ./dist/index.js",
"start:watch": "nodemon -e js,ts,tsx --ignore dist/ --ignore node_modules/ --exec npm start"

For development, we will run the last command:

npm run start:watch

This will launch the application and also allow for hot reloading whilst developing. After running the above command, you may have noticed a new window load. This window is your shiny new cross-platform React NodeGUI desktop application, which may not look like much at the moment, but we are about to change that.

Globals and systems details helper

The first thing that we want to do is to create a globals.ts file, where we will store some global information related to our application. In the src directory, create a directory called helpers, and within the directory, create a file called globals.ts and add the following code:

const colors = {
    red: '#FF652F',
    yellow: '#FFE400',
    green: '#14A76C'
}

const labels = {
    free: 'Free',
    used: 'Used'
}

export const globals = {      
    colors,
    labels
}

In the above code snippet, we create two objects, colors and labels. These are added to the globals object, which is then exported.

Notice that we only use the colors and labels variable names in the globals object; this is the Object property value shorthand in ES6.

If you want to define an object whose keys have the same name as the variables passed in as properties, you can use the shorthand and simply pass the key name.

The export statement is used when creating JavaScript modules to export functions, objects, or primitive values from the module so they can be used by other programs with the import statement.

Next, we can put the globals.ts file to use in the systemDetails.ts file, which we can also create in the helpers directory:

// Import External Dependencies
const osu = require('node-os-utils')

// Destructure plugin modules
const {os, cpu, mem, drive} = osu
 
// Import Globals
import { globals } from "./globals"
 
// Use ASYNC function to handle promises
export const systemDetails = async () => {
    // Static Details
    const platform = cpu.model()
    const operatingSystem = await os.oos()
    const ip = os.ip()
    const osType = os.type()
    const arch = os.arch()

    // CPU Usage
    const cpuUsed= await cpu.usage()
    const cpuFree = await cpu.free()

    // Memory Usage
    const memUsed = await mem.used()
    const memFree = await mem.free()

    // Disk Space Usage
    const driveInfo = await drive.info()
    const memUsedPercentage = memUsed.usedMemMb / memUsed.totalMemMb * 100
    const memFreePercentage = memFree.freeMemMb / memFree.totalMemMb * 100

    const systemInformation = {
      staticDetails: {
        platform,
        operatingSystem,
        ip,
        osType,
        arch
      },
      cpuDetails: {
        cpuUsed: {
          usage: cpuUsed,
          label: globals.labels.used,
          color: globals.colors.red
        },
        cpuFree: {
          usage: cpuFree,
          label: globals.labels.free,
          color: globals.colors.green
        }
      },
      memoryDetails: {
        memUsed: {
          usage: memUsedPercentage,
          label: globals.labels.used,
          color: globals.colors.red
        },
        memFree: {
          usage: memFreePercentage,
          label: globals.labels.free,
          color: globals.colors.green
        }
      },
      driveDetails: {
        spaceUsed: {
          usage: driveInfo.usedPercentage,
          label: globals.labels.used,
          color: globals.colors.red
        },
        spaceFree: {
          usage: driveInfo.freePercentage,
          label: globals.labels.free,
          color: globals.colors.green
        }
      }
    }
    return systemInformation
}

This may seem like a lot, but there is actually not so much going on. We will break down the code, line by line.

Firstly, we require the node-os-utils nom package, which we will use to get all our system information.

As stated by the package description, “Some methods are wrappers of node libraries and others are calculations made by the module,” meaning the package relies mainly on native Node.js libraries, which makes it very compatible with NodeGUI. Read more about this here.

Next, we use JavaScript ES6 destructuring to assign variables to functions that we will be using from the node-os-utils package.

Next, we import the globals object that we created ourselves. Just like we used the export statement in the globals.ts file, we now use it again, but this time to export the ASYNC function systemDetails.

The node-os-utils library mostly uses JavaScript with ES6 promises to return data, which allows us to retrieve that data using an async/await function. This allows us to write completely synchronous-looking code while performing asynchronous tasks behind the scenes.

I find that using async/await functions lead to very clean, concise, and readable code, so if you are not using them already, definitely check it out. Here is an awesome video explanation of async/await.

We use the node-os-utils library to get our system’s information. Notice that we use the await operator in front of some function calls; these are the functions returning a promise. In the node-os-utils libraries description, you can see exactly what each function call returns.

Functions In node-os-utils
node-os-utils functions.

We then use all the values returned from the function calls to create the systemInformation object, which is returned by the systemDetails function.

We are now ready to use systemInformation and create the application interface.

Application interface and design

As you may have noticed, at this stage, our application does not look like much — but we are about to change that.

In the src directory of our application, create a components directory and the following three component files:

  • InnerContainer.tsx
  • StatsColumn.tsx
  • StatsRow.tsx

Next, we will need to update the index.tsx file in the src directory, and instead of providing all the code, I shall provide snippets to be added with explanations along the way.

To start, let’s remove all the code that we will not use for our application, leaving us with an clean index.tsx file, as below:

// Import External Dependencies
import {Window, Renderer, View, Text} from "@nodegui/react-nodegui"
import React, { useState, useEffect } from "react"

// Import System Details
import { systemDetails } from "./helpers/systemDetails"

// Application width and height
const fixedSize = { width: 490, height: 460 }

// Function React Component
const App = () => {
  return (
    <Window minSize={fixedSize} maxSize={fixedSize} styleSheet={styleSheet}>
      <View id="container">
        <Text id="header">System Utility Monitor</Text>
      </View>
    </Window>
  )
}

// Application Stylesheets
const styleSheet = `
  #container {
    flex: 1;
    flex-direction: column;
    min-height: '100%';
    align-items: 'center';
    justify-content: 'center';
  }
`

// Render the application
Renderer.render(<App />)

If you have worked with React Native before, the above syntax might seem familiar: similar to React Native, we don’t have the freedom to work with HTML. Instead, we work with predefined components (View, Text, etc.) provided by the framework.

In the above code, we once again import modules and functions using the JavaScript ES6 destructuring syntax.

We then declare a constant, fixedSize, which we will use to assign a minimum and maximum width to our application window.

We then create a functional React component where we will build the application. This tutorial will not explain the basics of React, but you can get a beginner’s tutorial here. This was one of the few video tutorials that make use of React Hooks, which we will use.

If you want to go deeper into React theory, here is an excellent article detailing the intricacies of React functional components. Also check out the official React documentation on React Hooks, which is available from React 16.8 and are an excellent addition to the framework.

The first component from the NodeGUI React framework is the <Window/> component.

A QMainWindow provides a main application window. Every widget in NodeGui should be a child/nested child of QMainWindow. QMainWindow in NodeGui is also responsible for FlexLayout calculations of its children.

We provide the <Window/> component minSize, maxSize, and styleSheet props. The styleSheet constant is declared on line 22.

Nested within the <Window/> component is a <View/> component, and within it is a <Text/> component.

<View/> component:

A QWidget can be used to encapsulate other widgets and provide structure. It functions similar to a div in the web world.

<Text/> component:

A QLabel provides ability to add and manipulate text.

We then declare a styleSheet constant, which is a template literal string.

Template literals are string literals allowing embedded expressions. You can use multi-line strings and string interpolation features with them. They were called “template strings” in prior editions of the ES2015 specification.

Styling the application proved to be rather tricky, as not all CSS properties are supported by the NodeGUI framework, and in some cases, one needs to refer to Qt Documents to see exactly what one can use.

For example, the property overflow:scroll does not exist in Qt CSS, so one needs to implement other workarounds for this functionality as per this GitHub issue thread.

Regarding flexbox support, the NodeGUI framework supports all properties and all layouts as per the Yoga Framework, which is also used by frameworks like React Native and ComponentKit.

Lastly, we render our application.

Now that the base of our application is in place, we will need to integrate the system information and display it using the components we created.

Initial data object for React Hooks

Before we can use the system data, we will need an initial data object, which the application will use before being populated with data returned from the systemDetails function. In the helpers directory, create a new file initialData.ts and add the following code:

export const initialData = {
    staticDetails:{
      platform: 'Loading System Data...',
      operatingSystem: '',
      ip: '',
      osType: '',
      arch: ''
    },
    cpuDetails:{
      cpuUsed: {
        usage: '',
        label: 'Loading',
        color: ''
      },
      cpuFree: {
        usage: '',
        label: 'Loading',
        color: ''
      }
    },
    memoryDetails:{
      memUsed: {
        usage: '',
        label: 'Loading',
        color: ''
      },
      memFree: {
        usage: '',
        label: 'Loading',
        color: ''
      }
    },
    driveDetails: {
      spaceUsed: {
        usage: '',
        label: 'Loading',
        color: ''
      },
      spaceFree: {
        usage: '',
        label: 'Loading',
        color: ''
      }
    }
  }

As you can see this mimics the systemInformation object which is returned by the systemDetails function. Lets add this to the index.ts file with as follows:

...
// Import System Details
import { systemDetails } from "./helpers/systemDetails"
import { initialData } from "./helpers/initialData"
...

Putting the data to use

Cue React Hooks, probably one of my favorite developments in the JavaScript ecosystem in the last couple of years. It allows for clear and concise code that is very readable and maintainable.

Let’s gets started by implementing the React setState Hook that we imported earlier. Add the following code inside the App functional React component:

  // Array destructure data and setData function
  const [data, setData] = useState(initialData)

There is a lot to unpack here, especially if you are new to React Hooks. Instead of trying to explain it all here, I am including a video as a quick introduction course:

If we console.log() the data constant, we will see that our initialData object has been assigned to the data constant.

Now let’s use some destructuring again to assign the variables we will need for the static data within our application:

  //Get Static Data
  const {platform, operatingSystem, ip, osType, arch} = data.staticDetails

Currently, the data constant is still pointing to the initialData object we created. Let’s use the useEffect() Hook to update our state with data from the systemsDetail function. We can do this by adding the following code to the index.tsx file, right after the useState() Hook:

...
const [data, setData] = useState(initialData)

useEffect(() => {
  const getSystemData = async () => {
    const sysData : any = await systemDetails()
    setData(sysData)
  }
  getSystemData()
})

//Get Static Data
...

Now if we now console.log() the data constant, we will see that it is constantly being updated with new data!

Once again, we will not go into the theory behind the code, but definitely read up on the useEffect() Hook and async/await functionality.

We can now add the following code below the application header, which will display the system platform:

<Text id="subHeader">{platform}</Text>

The base foundation for our application has been laid. All we need to do now is the construction and decoration.

Styling and components

Let’s start by replacing the styleSheet constant in the index.tsx file with the following code:

// Application Stylesheets
const styleSheet = `
  #container {
    flex: 1;
    flex-direction: column;
    min-height: '100%';
    height: '100%';
    justify-content: 'space-evenly';
    background-color: #272727;
  }
  #header {
    font-size: 22px;
    padding: 5px 10px 0px 10px;
    color: white;
  }
  #subHeader {
    font-size: 14px;
    padding: 0px 10px 10px 10px;
    color: white;
  }
`

So far this is pretty standard CSS styling, but we will see some edge cases as we proceed.

Let’s populate our first component, the StatsRow.tsx file, with the following code:

// Import External Dependencies
import React from 'react'
import {View} from "@nodegui/react-nodegui"

export const StatsRow = (props: { children: React.ReactNode; }) => {
  return (
      <View id="systemStats" styleSheet={styleSheet}>
          {props.children}
      </View>
  )
}

const styleSheet = `
  #systemStats {
    width: 470;
    height: 180;
    flex: 1;
    flex-direction: row;
    justify-content: 'space-between';
    margin-horizontal: 10px;
  }
`

We have covered most of the the code above, but one thing to note is the special React prop props.children and the syntax for using it with TypeScript. This article has a very in-depth explanation regarding React children composition patterns in TypeScript.

Let’s import the StatsRow component by adding the following code to the index.tsx file:

...
// Import Components
import {StatsRow} from "./components/StatsRow"
...

We will use the StatsRow component to create two rows in our application, but before we use it, let’s first populate the innerContainer.tsx by adding the following code:

// Import External Dependencies
import React from 'react'
import {View, Text} from "@nodegui/react-nodegui"

// Set Types
type InnerContainerColumnProps = {
    title: string
}

export const InnerContainer: React.FC<InnerContainerColumnProps> = props => {
  // Desctructure props
  const {title, children} = props

  return (
      <View id="innerContainer" styleSheet={styleSheet}>        
          <Text id="headText">{title}</Text>
          <View id="stats">
            {children}
          </View>
      </View>
  )
}

const styleSheet = `
  #innerContainer {
    height: 180;
    width: 230;
    background: #111111;
    border-radius: 5px;
  }
  #stats {
    flex-direction: row;
    align-items: 'flex-start';
    justify-content: 'flex-start';
  }

  #headText {
      margin: 5px 5px 5px 0;
      font-size: 18px;
      color: white;
  }
`

Again, we covered most of the above code already. Notice that we need to take some extra measures to accommodate TypeScript in the React components — this is an excellent article explaining the best ways of making the components and TypeScript work together.

Let’s add it to the index.tsx file with the following code:

...
// Import Components
import {StatsRow} from "./components/StatsRow"
import {InnerContainer} from "./components/InnerContainer"
...

Let’s finish up our final component, StatsColumn.tsx, before tying it all together in the index.tsx file. I will break up the code into two parts, which should be combined: the first part is the component without the styles, and the second part is the styles:

// Import External Dependencies
import React from 'react'
import {View, Text} from "@nodegui/react-nodegui"

// Set Types
type StatsColumnProps = {
    label: string,
    usage: number,
    color: string
}

export const StatsColumn: React.FC<StatsColumnProps> = props => {
    // Destructure props
    const {usage, color, label} = props

    // Create Label with usage amount and percentage
    const percentageTextLabel = `${label} ${Math.round(usage * 100) / 100}%`

    // Create Dynamic Style Sheet
    const dynamicStyle = `
        height: ${usage};
        background-color: ${color};
    `

    return (
        <View id="statsContainer" styleSheet={statsContainer}>
            <View id="columnContainer" styleSheet={columnContainer}>   
                <View id="innerColumn" styleSheet={dynamicStyle}></View>
            </View>
            <Text id="statsLabel" styleSheet={statsLabel}>{percentageTextLabel}</Text>
        </View>
    )
}

We use this component to create the graph effect, as you can see on the final application screen grab.

We pass the label, usage, and color props to the component, which we will use to dynamically update the component.

Below the above code, add the style code below:

const statsContainer = `
    #statsContainer {
        height: '140';
        text-align:center;
        justify-content: 'center';
        align-items: 'center';
        justify-content: 'space-between';
        width: 100%;
        flex: 1 0 100%;
        margin-horizontal: 5px;
    }
`

const columnContainer = `
    #columnContainer{
        height: 100%;
        flex: 1 0 100%;
        flex-direction: column-reverse;
        background-color: #747474;
        width: 100%;
    }
`

const statsLabel = `
    #statsLabel {
        height: 40;
        color: white;
        font-size: 14px;
        width: 100%;        
        qproperty-alignment: 'AlignCenter';
        color: white;
    }
`

Note how each style property is declared as its own constant. This is another way to create styleSheet blocks; I doubt it makes a difference, it is more a developer preference.

You may also have noticed the CSS property qproperty-alignment: 'AlignCenter'; and thought you have not seen this before. And you are totally right — this is a Qt property and it is used to align text. It took me some time to figure this out. Here is a Qt style sheet syntax reference link, which could assist you if you encounter a caveat like this.

That’s it for the components. Let’s get to work on the index.tsx file.

Let’s wrap this up

Let’s import our final component into the index.tsx file:

// Import Components
import {StatsRow} from "./components/StatsRow"
import {InnerContainer} from "./components/InnerContainer"
import {StatsColumn} from "./components/StatsColumn"

Add the following styles to the styleSheet constant in the index.tsx file:

...
  #subHeader {
    font-size: 14px;
    padding: 0px 10px 10px 10px;
    color: white;
  }

  #headText {
    margin: 5px 5px 5px 0;
    font-size: 18px;
    color: white;
  }
  #infoText {
    padding: 5px 0 0 5px;
    color: white;
  }
  #informationContainer {
    height: 180;
    width: 230;
    background: #111111;
    border-radius: 5px;
  }
...

Now for the first bit of meat on our application. Below the <Text id="subHeader"> component in the index.tsx file, add the following code:

...
<StatsRow>
   <View id="informationContainer" styleSheet={styleSheet}>
      <Text id="headText">System Information</Text>
      <Text id="infoText">{operatingSystem}</Text>
      <Text id="infoText">{osType}</Text>
      <Text id="infoText">{ip}</Text>
      <Text id="infoText">{arch}</Text>
    </View>
</StatsRow>
...

The above code is pretty self-explanatory, but notice that we need to reference the styleSheet in the <View id="informationContainer"> , even after referencing it in the main <Window> component. This is due to a caveat where the styles are not inherited by children components.

If you are “still watching” the application, you will now see that, for the first time, our application is starting to resemble an actual application.

Let’s add the code to create the “charts.” Below the useEffect() Hook, add the following code:

const renderCpuDetails = () => {
  const cpuDetails = data.cpuDetails
  return Object.keys(cpuDetails).map((key) => {
      const stat = cpuDetails[key]
      return <StatsColumn label={stat.label} usage={stat.usage} color={stat.color}  />
  })
}

const renderMemoryDetails = () => {
  const memDetails = data.memoryDetails
  return Object.keys(memDetails).map((key) => {
      const stat = memDetails[key]
      return <StatsColumn label={stat.label} usage={stat.usage} color={stat.color}  />
  })
}

const renderDriveDetails = () => {
  const driveDetails = data.driveDetails
  return Object.keys(driveDetails).map((key) => {
      const stat: any = driveDetails[key]
      return <StatsColumn label={stat.label} usage={stat.usage} color={stat.color}  />
  })
}

In the above code, we loop over the respective object keys and then use the values as props for the <StatsColumn/> component.

We can then use these functions in our code by updating the index.tsx file with the following:

<StatsContainer>
    <View id="informationContainer" styleSheet={styleSheet}>
      <Text id="headText">System Information</Text>
      <Text id="infoText">{operatingSystem}</Text>
      <Text id="infoText">{osType}</Text>
      <Text id="infoText">{ip}</Text>
      <Text id="infoText">{arch}</Text>
    </View>
  <InnerContainer title={"Disk Space"}>
    {renderDriveDetails()}
  </InnerContainer>
</StatsContainer>
<StatsContainer>
  <InnerContainer title={"CPU Usage"}>
    {renderCpuDetails()}
  </InnerContainer>
  <InnerContainer title={"Memory Usage"}>
    {renderMemoryDetails()}
  </InnerContainer>
</StatsContainer>

In the above code, we execute the three previously declared functions, which, in turn, render the Disk Space, CPU Usage, and Memory Usage columns.

That wraps up our application, the source code for everything can be found here on GitHub.

Conclusion

Having been announced for release just two months ago, React NodeGUI is still very much in their infancy, but with more than 3,500 stars on GitHub at the time of writing, it definitely shows a lot of promise.

As a web developer, one might be very accustomed to writing HTML code and switching to the React Native-like component approach does demand a bit of a mindset shift since one does not have the freedom of HTML.

Some components, like the Qt Scroll Area, still need to be ported to the framework, so if one is to start a project with the framework, first thoroughly research the limitations and also keep an eye on the issues on GitHub.

The last bit of advice is to not take anything for granted. To ensure a truly cross-platform desktop experience, make sure that all CSS properties are explicitly declared — this means all colors, fonts, font-sizes, etc. are all specified, as it may be interpreted differently by different operating systems.

Plug: , a DVR for web apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.

.
Siegfried Grimbeek Web developer, open source enthusiast, agile evangelist, and tech junkie. Currently hacking away at the frontend for Beerwulf 🙌

Leave a Reply