Written by Siegfried Grimbeek✏️
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:
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.
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 adiv
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.
Editor's note: Seeing something wrong with this post? You can find the correct version here.
Plug: LogRocket, a DVR for web apps
LogRocket is a frontend logging tool 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.
Try it for free.
The post Electron alternatives: Exploring NodeGUI and React NodeGUI appeared first on LogRocket Blog.
Top comments (0)