Creating standalone Desktop Applications with React, Electron and SQLite3

There are quite a few tutorials on the Internet that cover the process of setting up React inside an Electron app but very few (if any) cover the solutions to common problems you run in to when packaging the app for distribution. In this blog, I am going to run you through how I setup a production ready work flow for creating a desktop app with React in Electron and SQLite3 as a database packaged with the application.

In my most recent personal project, I built a Daily Build Updater for the 3D application Blender. This retrieves the latest daily build, compares it with your existing installation, lets you update or reinstall, clean up older installations and setup your shortcuts. A one click solution to automate the entire process. You can check out a preview of the app below.

Let’s get started.

First and foremost, let us get React. The simplest and best way to do it would be using the Create React App. I will be using yarn. You can use npm.

yarn create react-app MyDesktopApp
cd MyDesktopApp

Now that we have React ready to go, let’s setup Electron. We’ll need to install a few packages for that.

Development Dependencies:

  • electron —
  • electron-builder —
  • nodemon —
  • concurrently —
  • wait-on —

Dependencies:

  • electron-is-dev —
  • sqlite3 —
yarn add electron electron-builder nodemon concurrently wait-on -D
yarn add electron-is-dev

We have the packages we need. We can get started with setting up Electron.

In your public folder, create a two new files called electron.js and preload.js. If you care to know, the reason we are creating it in this folder specifically is because when you build the React app, all contents of this folder get carried over to the folder which makes things very simple for production. Additionally, any changes to files in this folder will trigger hot reload during development which can be very handy considering changes to the base Electron setup requires a full relaunch anyway.

The electron.js file will have the following code. It’s basic Electron setup but I’ll add comments where explanation is necessary.

Now that we have defined the Electron window, let us test out the app by booting it in development mode. Go to package.json and add the following.

By setting main, you are letting Electron know where to get the info for the window from and by setting homepage, you’re letting React know to build the app keeping in mind that the base location is the default location — .

Now we need a few scripts in package.json to be changed and added. Rename “start” to “start-react” and build to “build-react” and then add the following. Your scripts should look something like this.

To explain what they do —

  • start-react —
  • build-react —
  • start-electron —
  • dev —
  • postinstall —
  • pack-app —
  • build-
  • test —
  • eject —

Now just do yarn dev and it should boot up your React app inside an Electron Window.

Now that we have our development phase setup. Let us also set our build settings for production.

We need to add a build category to our package.json for electron-builder to know how we want our app built. There’s tons of options but here’s some basic ones for a Windows app. Refer to the link above for the full electron-builder documentation which is pretty clear.

To explain the above code — UNDERSTAND THIS — it’ll make your life a lot easier distributing the product -

  • build category —
  • appId —
  • productName —
  • copyright —
  • files —
  • directories —
  • extraResources —

  • win —

Communication Between React & Electron

So the important part. How do we communicate between React and Electron? Most tutorials online would recommend that you on your Electron app and use the / directly to talk to your frontend.

But it is NOT safe to expose your Electron backend to the renderer. The same issue even persists with the usage of Electron remote. Electron themselves have recommended against this and the remote module has been deprecated in Electron 12 and will be removed in Electron 14.

So what is the solution? Well, that’s where the preload.js which we created and the contextIsolation: true web preference we set earlier come in.

Firstly, the contextIsolation. By enabling this, you’re isolating the Electron logic to run in a separate context to that of the front end you load. This will ensure that any rogue scripts do not have access to the all powerful Electron backend. But obviously, this would cut off the frontends access to the Electron backend.

So in order for these two sides to be able to talk to each other, we create a bridge. That might sound complicated but it really is not.

What you are doing is basically creating a simple API inside your preload.js by defining simple functions that are preloaded to the app. So the front end only has access to these functions only making your app secure.

In the above file, we loaded ipcRenderer and contextBridge from electron.

ipcRenderer will let us send signals which we will catch with ipcMain in electron.js.

contextBridge is basically creating a bridge between React and Electron with the already pre-defined functions that you’ve exposed to the front end. So React can only access these. Nothing else.

Keys used like ‘test-invoke’, ‘test-send’ and etc need to be unique so there’s no clashes in the signals sent.

So instead of using ipcRenderer directly in React, you will now use window.api.testInvoke(‘example argument) which will trigger ipcRenderer.invoke(‘test-invoke’, ‘example argument’) that you might be familiar with.

There’s 3 basic ways of communication — I’ll provide examples right after:

  1. invoke — You send data from the frontend, process it with ipcMain.handle on the backend and return information to the frontend.
  2. send — You send data from the frontend, process it in the backend with ipcMain.on and send back a reply when it is processed.
  3. on — You receive data from the backend event.sender and process that with the help of a callback function.

EXAMPLES:

Let’s say I want to enter username, password in React, process that info in Electron / Node and get back my profile information.

INVOKE

In preload.js

In ReactComponent.js

In electron.js

In the above example, when the user submits the form, the formHandler sends the form data via the windows.api.getProfileInfo. The getProfileInfo function we set in the preload will trigger the get-profile-details signal. The get-profile-details signal is caught in the electron.js file where the arguments are received, data is retrieved from the database, verified and then profile info is returned. This returned profile info is awaited for in the React app and if its not null, it is set to the React state.

SEND

In preload.js

In ReactComponent.js

In electron.js

Trigger quitApp() from the renderer. Quit the app using electron.js — Send a reply for funsies because who cares once the app is closed.

RECEIVE

In preload.js

In electron.js

In ReactComponent.js

In this example, we click a button which triggers the downloadFileHandler which triggers the downloadFile signal passing the data from the form.

The downloadFile signal sent from preload.js with the data is caught by the electron.js file where the data is sent to an imaginary module called ‘’ whose function ‘’ has a callback function for the progress called .

onProgress send back the progress info through an event sender called ‘get-download-progress’ which is caught by getDownloadProgress in preload.js. The ReactComponent is constantly listening to getDownloadProgress with useEffect and updating the progress data each time it updates to the state.

Adding a Database

We will be using SQLite3 for this particular application because we can pack it along with the app.

Use DB Browser for SQLite to create a new database. You can store it anywhere. I’d recommend creating a folder called db in your root folder.

NOTE: Make sure you add this folder to the extraResources path in the build category. Because we want to ship our database file along with our application.

We’ll be using Node.js’ sqlite3 to communicate with our database.

yarn add sqlite3

sqlite3 generally needs bindings to be rebuilt. Because we have postinstall for electron-builder already setup, installing it right now should have fixed that issue for us. But if it did not, then just run yarn postinstall and watch for the success message.

You can refer to the SQLite documentation on how to use it. It’s pretty straight forward.

Conclusion

That’s it. When you’re done with your app, do a yarn pack-app to pack and test or yarn build to build it for distribution.

Let me know if I buggered up anything anywhere and feel free to buzz me up for any queries. You can catch me on Twitter at @wykrhm