DEV Community

Tomasz Wegrzanowski
Tomasz Wegrzanowski

Posted on

2 1

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.

Image of Timescale

🚀 pgai Vectorizer: SQLAlchemy and LiteLLM Make Vector Search Simple

We built pgai Vectorizer to simplify embedding management for AI applications—without needing a separate database or complex infrastructure. Since launch, developers have created over 3,000 vectorizers on Timescale Cloud, with many more self-hosted.

Read more

Top comments (0)

nextjs tutorial video

Youtube Tutorial Series 📺

So you built a Next.js app, but you need a clear view of the entire operation flow to be able to identify performance bottlenecks before you launch. But how do you get started? Get the essentials on tracing for Next.js from @nikolovlazar in this video series 👀

Watch the Youtube series

👋 Kindness is contagious

Dive into an ocean of knowledge with this thought-provoking post, revered deeply within the supportive DEV Community. Developers of all levels are welcome to join and enhance our collective intelligence.

Saying a simple "thank you" can brighten someone's day. Share your gratitude in the comments below!

On DEV, sharing ideas eases our path and fortifies our community connections. Found this helpful? Sending a quick thanks to the author can be profoundly valued.

Okay