Electron Adventures: Episode 75: NodeGui React
Tags: javascript, electron, react, qt
Published: 2021-10-06 19:58:06

Let's continue exploring Electron alternatives. This time, [NodeGui](https://github.com/nodegui/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](https://github.com/nodegui/svelte-nodegui/issues/67), 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](https://github.com/nodegui/nodegui).

```shell
$ brew install make cmake
$ npx degit https://github.com/nodegui/react-nodegui-starter episode-75-nodegui-react
$ cd episode-75-react-nodegui
$ npm i
```

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:

```shell
$ npm run dev
$ npm run start
```

### `package.json`

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

```javascript
{
  "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"
  }
}
```

### `.babelrc`

There's small `.babelrc` after removing unnecessary stuff:

```javascript
{
  "presets": [
    ["@babel/preset-env", { "targets": { "node": "12" } }],
    "@babel/preset-react"
  ],
  "plugins": []
}
```

### `webpack.config.js`

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

```javascript
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
}
```

### `src/index.jsx`

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

```javascript
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()
  })
}
```

### 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!
```

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](https://reactnative.dev/), 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.

```javascript
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)
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ch9czgg6y6ax1c6thi0m.png)

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](https://github.com/taw/electron-adventures/tree/master/episode-75-nodegui-react).