Electron Adventures: Episode 75: NodeGui React

taw

Tomasz Wegrzanowski

Posted on October 6, 2021

Electron Adventures: Episode 75: NodeGui React

Let's continue exploring Electron alternatives. This time, NodeGui. NodeGui uses Qt5 instead of Chromium, so we'll be leaving the familiar web development behind, but it tries to not be too far from it, as web development is what everyone knows.

Interestingly it comes with preconfigured Svelte, React, and Vue setups, but since Svelte starter doesn't work at all, we'll try out the React one.

Installation

We need to install a bunch of dependencies, not just npm packages. For OSX this one extra line of brew is required. For other OSes, check documentation.

$ brew install make cmake
$ npx degit https://github.com/nodegui/react-nodegui-starter episode-75-nodegui-react
$ cd episode-75-react-nodegui
$ npm i
Enter fullscreen mode Exit fullscreen mode

Unfortunately instead of having happy React started, what we get at this point is some T***Script abomination, so next few steps were me ripping out T***Script and putting back plain JavaScript in its place.

Start the app

To start the app we'll need to run these in separate terminals:

$ npm run dev
$ npm run start
Enter fullscreen mode Exit fullscreen mode

package.json

Stripped out of unnecessary dependencies, here's what's left:

{
  "name": "react-nodegui-starter",
  "main": "index.js",
  "scripts": {
    "build": "webpack -p",
    "dev": "webpack --mode=development",
    "start": "qode ./dist/index.js",
    "debug": "qode --inspect ./dist/index.js"
  },
  "dependencies": {
    "@nodegui/react-nodegui": "^0.10.2",
    "react": "^16.13.1"
  },
  "devDependencies": {
    "@babel/core": "^7.11.6",
    "@babel/preset-env": "^7.11.5",
    "@babel/preset-react": "^7.10.4",
    "@nodegui/packer": "^1.4.1",
    "babel-loader": "^8.1.0",
    "clean-webpack-plugin": "^3.0.0",
    "file-loader": "^6.1.0",
    "native-addon-loader": "^2.0.1",
    "webpack": "^4.44.2",
    "webpack-cli": "^3.3.12"
  }
}
Enter fullscreen mode Exit fullscreen mode

.babelrc

There's small .babelrc after removing unnecessary stuff:

{
  "presets": [
    ["@babel/preset-env", { "targets": { "node": "12" } }],
    "@babel/preset-react"
  ],
  "plugins": []
}
Enter fullscreen mode Exit fullscreen mode

webpack.config.js

And here's similarly cleaned up webpack.config.js:

const path = require("path")
const webpack = require("webpack")
const { CleanWebpackPlugin } = require("clean-webpack-plugin")

module.exports = (env, argv) => {
  const config = {
    mode: "production",
    entry: ["./src/index.jsx"],
    target: "node",
    output: {
      path: path.resolve(__dirname, "dist"),
      filename: "index.js"
    },
    module: {
      rules: [
        {
          test: /\.jsx?$/,
          exclude: /node_modules/,
          use: {
            loader: "babel-loader",
            options: { cacheDirectory: true, cacheCompression: false }
          }
        },
        {
          test: /\.(png|jpe?g|gif|svg|bmp|otf)$/i,
          use: [
            {
              loader: "file-loader",
              options: { publicPath: "dist" }
            }
          ]
        },
        {
          test: /\.node/i,
          use: [
            {
              loader: "native-addon-loader",
              options: { name: "[name]-[hash].[ext]" }
            }
          ]
        }
      ]
    },
    plugins: [new CleanWebpackPlugin()],
    resolve: {
      extensions: [".js", ".jsx", ".json"]
    }
  }

  if (argv.mode === "development") {
    config.mode = "development";
    config.plugins.push(new webpack.HotModuleReplacementPlugin());
    config.devtool = "source-map";
    config.watch = true;
    config.entry.unshift("webpack/hot/poll?100");
  }

  return config
}
Enter fullscreen mode Exit fullscreen mode

src/index.jsx

This is reasonably close to what we would use in plain React.

import { Renderer } from "@nodegui/react-nodegui"
import React from "react"
import App from "./app"

process.title = "My NodeGui App"
Renderer.render(<App />)
// This is for hot reloading (this will be stripped off in production by webpack)
if (module.hot) {
  module.hot.accept(["./app"], function() {
    Renderer.forceUpdate()
  })
}
Enter fullscreen mode Exit fullscreen mode

Hot module reloading

Important thing to note is hot module reloading we enabled.

You can use hot module reloading in Electron as well, but you can also use Cmd-R to reload manually, so it's nice but unnecessary.

NodeGUI has no such functionality, so you're very dependent on hot module reloading for development to be smooth. Unfortunately if you ever make a syntax error in your code, you get this:

[HMR] You need to restart the application!
Enter fullscreen mode Exit fullscreen mode

And you'll need to quit the application, and start it again.

So in practice, the dev experience is a lot worse than default Electron experience.

src/app.jsx

And finally we can get to the app.

Similar to how React Native works, instead of using html elements, you need to import components from @nodegui/react-nodegui.

The nice thing is that we can declare window properties same as any other widgets, instead of windows being their own separate thing. Some APIs differ like event handling with on={{...}} instead of individual onEvent attributes.

A bigger issue is the Qt pseudo-CSS. It supports different properties from HTML (so there's now "How to center in Qt" question, which you can see below), and unfortunately it doesn't seem to support any element type or class based selectors, just attaching to an element with style or using ID-based selectors. There's probably some way to deal with this.

import { Text, Window, hot, View, Button } from "@nodegui/react-nodegui"
import React, { useState } from "react"

function App() {
  let [counter, setCounter] = useState(0)

  return (
    <Window
      windowTitle="Welcome to NodeGui"
      minSize={{ width: 800, height: 600 }}
      styleSheet={styleSheet}
    >
      <View style={containerStyle}>
        <Text id="header">Welcome to NodeGui</Text>
        <Text id="text">The button has been pressed {counter} times.</Text>
        <Button id="button" on={{
          clicked: () => setCounter(c => c+1)
        }}>CLICK ME!</Button>
        <Text id="html">
          {`
            <p>For more complicated things</p>
            <ul>
              <li>Use HTML</li>
              <li>Like this</li>
            </ul>
          `}</Text>
      </View>
    </Window>
  )
}

let containerStyle = `
  flex: 1;
`

let styleSheet = `
  #header {
    font-size: 24px;
    padding-top: 20px;
    qproperty-alignment: 'AlignHCenter';
    font-family: 'sans-serif';
  }

  #text, #html {
    font-size: 18px;
    padding-top: 10px;
    padding-horizontal: 20px;
  }

  #button {
    margin-horizontal: 20px;
    height: 40px;
  }
`

export default hot(App)
Enter fullscreen mode Exit fullscreen mode

Overall this wasn't too bad a change from plain React. We can still structure the components the same way, use either hooks or classes for state, and also import any frontend JavaScript libraries we want.

Results

Here's the results:

Episode 75 Screenshot

After all the work setting up Nodegui with React and plain JavaScript it would be a shame not to write a small app with it, so in the next episode we'll do just that.

As usual, all the code for the episode is here.

💖 💪 🙅 🚩
taw
Tomasz Wegrzanowski

Posted on October 6, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related