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
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
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"
}
}
.babelrc
There's small .babelrc
after removing unnecessary stuff:
{
"presets": [
["@babel/preset-env", { "targets": { "node": "12" } }],
"@babel/preset-react"
],
"plugins": []
}
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
}
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()
})
}
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, 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)
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:
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.