DEV Community

loading...

Start a new Electron app with React and Typescript.

elisealcala profile image Elizabeth Alcalá ・3 min read

What is Electron?

Electron is a framework for creating native applications. It's open-source and cross-platform. If you already know Javascript, HTML, and CSS you can build an application with electron.

In this tutorial, I'll show you how to start an electron project from scratch using webpack, react, and Typescript.

Start with Electron.

Let's start by creating a new folder and a new npm project.

mkdir electron-react-ts
cd electron-react-ts
npm init -y

Now install these dependencies.

npm install --save-dev electron \
webpack webpack-cli webpack-dev-server \
babel-loader @babel/core @babel/preset-env \
@babel/preset-react @babel/preset-typescript

Create a tsconfig.json file. This allows you to specify the configuration for the typescript compiler.

{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "lib": [
      "dom",
      "es2015",
      "es2016",
      "es2017"
    ],
    "allowJs": true,
    "jsx": "react",
    "sourceMap": true,
    "outDir": "./dist",
    "strict": true,
    "esModuleInterop": true,
  }
}

Create a babel.config.js and an index.html file at the root of our app.

module.exports = {
  presets: [
    '@babel/preset-env',
    '@babel/preset-react',
    '@babel/preset-typescript'
  ]
}
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>New Electron App</title>
</head>
<body>
</body>
</html>

Let's create a new file called webpack.electron.config.js on the root of our app. This webpack file will compile our electron app into a dist folder.

const path = require('path');

module.exports = {
  resolve: {
    extensions: ['.tsx', '.ts', '.js'],
  },
  devtool: 'source-map',
  entry: './electron/main.ts',
  target: 'electron-main',
  module: {
    rules: [
      {
        test: /\.(js|ts|tsx)$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
        },
      },
    ],
  },
  output: {
    path: path.resolve(__dirname, './dist'),
    filename: '[name].js',
  },
};

This looks like a normal webpack configuration for typescript, except for the target. The target is the specific environment that webpack will compile for. In this case it's electron-main.

Create an electron folder, then inside a main.ts file with the following code.
This file should create windows and handle the systems events for your app.

import { app, BrowserWindow } from 'electron';
import * as path from 'path';
import * as url from 'url';

let mainWindow: Electron.BrowserWindow | null;

function createWindow() {
  mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      nodeIntegration: true,
    },
  });

  if (process.env.NODE_ENV === 'development') {
    mainWindow.loadURL(`http://localhost:4000`);
  } else {
    mainWindow.loadURL(
      url.format({
          pathname: path.join(__dirname, '../index.html'),
          protocol: 'file:',
          slashes: true
      })
    );
  }

  mainWindow.on('closed', () => {
    mainWindow = null;
  });
}

app.on('ready', createWindow);
app.allowRendererProcessReuse = true;

The BrowserWindow module will create a new window and render our react app.

Now let's add a script in the package.json file in order to run electron. Also, we have to change the main field for the path that has our electron app compiled.

{
  "main": "./dist/main.js",
    "scripts": {
    "dev:electron": "NODE_ENV=development webpack --config webpack.electron.config.js --mode development && electron ."
  },
}

Now run npm run dev:electron in the console.

Note: If you are using Windows, chances are you'll face an error, this is because NODE_ENV is not recognized as a command. You have to install crossenv and place the command before NODE_ENV.

Add a React app.

Now that we have our electron app running, let's set up a react app to run within this electron context.

We need to install a few dependencies.

npm install react react-dom @types/react @types/react-dom

npm install --save-dev html-webpack-plugin

Create a new webpack.react.config.js file.

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  resolve: {
    extensions: ['.tsx', '.ts', '.js'],
    mainFields: ['main', 'module', 'browser'],
  },
  entry: './src/app.tsx',
  target: 'electron-renderer',
  devtool: 'source-map',
  module: {
    rules: [
      {
        test: /\.(js|ts|tsx)$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
        },
      },
    ],
  },
  devServer: {
    contentBase: path.join(__dirname, '../dist/renderer'),
    historyApiFallback: true,
    compress: true,
    hot: true,
    port: 4000,
    publicPath: '/',
  },
  output: {
    path: path.resolve(__dirname, '../dist/renderer'),
    filename: 'js/[name].js',
    publicPath: './',
  },
  plugins: [
    new HtmlWebpackPlugin(),
  ],
};

Our package.json file now should look like this.

{
  "main": "./dist/main.js",
    "scripts": {
      "dev:electron": "NODE_ENV=development webpack --config webpack.electron.config.js --mode development && electron .",
      "dev:react": "NODE_ENV=development webpack-dev-server --config webpack.react.config.js --mode development"
    },
 }

In order to try this app, let's create a new folder src with anapp.tsx file inside.

import React from 'react';
import ReactDom from 'react-dom';

const mainElement = document.createElement('div');
document.body.appendChild(mainElement);

const App = () => {
  return (
    <h1>
      Hi from a react app
    </h1>
  )
}

ReactDom.render(<App />, mainElement);

Now we are ready.
Run npm run dev:react in one console, and npm run dev: electron on other one.

Electron app

Check this repo for the code.

Discussion (21)

Collapse
raphael10collab profile image
raphael10-collab • Edited

Once I set nodeIntegration: false, I get this error:

external "url":1 Uncaught ReferenceError: require is not defined
     at Object.url (external "url":1)
    at __webpack_require__ (bootstrap:789)
    at fn (bootstrap:100)
    at Object../node_modules/webpack-dev-server/client/utils/createSocketUrl.js 
(createSocketUrl.js:4)
    at __webpack_require__ (bootstrap:789)
    at fn (bootstrap:100)
    at Object.<anonymous> (client:20)
    at Object../node_modules/webpack-dev-server/client/index.js?http://localhost:4000  
(main.js:36416)
    at __webpack_require__ (bootstrap:789)
        at fn (bootstrap:100)
Collapse
elisealcala profile image
Elizabeth Alcalá Author

That's weird, what if you don't set nodeIntegration? does the error still there?

Collapse
raphael10collab profile image
raphael10-collab

I wrote this github's issue about this problem: github.com/elisealcala/electron-re...

I think it's an important issue, because if we want, for strict security reasons, keep nodeIntegration:false + contextIsolation:true, we must solve it

Collapse
raphael10collab profile image
raphael10-collab • Edited

Once i set nodeIntegration: true again, the error disappears. So It Is clearly related to nodeIntegration: false

Collapse
raphael10collab profile image
raphael10-collab • Edited

So... my question is: how to solve the problem, while keeping nodeIntegration:false + contextIsolation:true for strict security reasons?

Collapse
raphael10collab profile image
raphael10-collab

I "solved" by changing in webpack.react.config.js :

target:
//"electron-renderer",
"web",

Does this change causes some side-effects?

Thread Thread
elisealcala profile image
Elizabeth Alcalá Author

Hi, maybe you will have problems when you build your app since that is what electron-renderer is used for. Do you have a repo to see your code? do you mind sharing it on the issue you opened? thanks.

Thread Thread
raphael10collab profile image
raphael10-collab • Edited

I loaded everything here: github.com/raphael10-collab/electr...
As you can see, I didn't anything special: it's your app, where the only differences are these ones:

  • electron/main.ts :

    webPreferences: {
    nodeIntegration: false

  • webpack.react.config.js :

    target:
    //"electron-renderer",
    "web",

If you revert back webpack.react.config.js from "web" to "electron-renderer", you should get the error:
"Uncaught ReferenceError: require is not defined at Object.url", when nodeIntegration is kept to false. If nodeIntegration is set to true, the problem disappears. But this is not what we are looking for, because we would like to have a more secure electron-react-typescript app with nodeIntegration:false

Thread Thread
raphael10collab profile image
raphael10-collab • Edited

I loaded everything on here: github.com/raphael10-collab/electr...

In your app I just modified these two things:

  • webPreferences:

    nodeIntegration: true

  • webpack.react.config.js :

    target:
    //"electron-renderer",
    "web",
    

If you revert back target: "web" to "electron-renderer" (the correct setting), you should get the error: "Uncaught ReferenceError: require is not defined at Object.url"
And if you set nodeIntegration:true the problem disappears. But if we want to keep our app more secure, we ought to keep nodeIntegration:false

Thread Thread
raphael10collab profile image
raphael10-collab • Edited

Removing the devServer part of webpack.react.config.js :
while keeping target: "electron-renderer" make it work also with nodeIntegration: false

webpack.react.config.js :

const path = require("path");
//const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
  resolve: {
    extensions: [".tsx", ".ts", ".js"],
    mainFields: ["main", "module", "browser"],
  },
  entry: "./src/app.tsx",
  target:
    "electron-renderer",
    //"web",
  devtool: "inline-source-map",
  module: {
    rules: [
      {
        test: /\.(js|ts|tsx)$/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader",
        },
      },
    ],
  },
  //devServer: {
    //contentBase: path.join(__dirname, "../dist/renderer"),
    //historyApiFallback: true,
    //compress: true,
    //hot: true,
    //port: 4000,
    //publicPath: "/",
  //},
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "js/[name].js",
  },
  //plugins: [new HtmlWebpackPlugin()],
};

This is the new repo: github.com/raphael10-collab/Electr...
git cloning it, and then executing yarn electron .
I get the app without any errors

Collapse
huncyrus profile image
huncyrus

Nice and easy tutorial.
Do you plan to continue it with adding more electron based functionality (to tease some capability of the electron itself) or discuss the webpack configuration to let even the beginners understand why-what-where to do in it?

Collapse
elisealcala profile image
Elizabeth Alcalá Author

Hey, I could write about the webpack configuration, maybe a detailed and general view to understand how it works.
I thought about making more electron posts, maybe a serie about creating a complete app.

Collapse
guarn profile image
Guarn

Hi, great article, thanks a lot !

I just have a question, I'm struggled with a simple import of .png after the complete installation. Everything works great, including adding styled components and other packages, but I simply can't import an image file, I have an error : "no loaders configured for this file". I guess I have to edit the Webpack conf, but don't know how :(

Can you help me ?

Collapse
serbroda profile image
Danny Rottstegge

Hey, really greate post. Exactly what I need.
Can you also describe how to pack it with electron-builder or electron-packager?

Thx in advice.

Collapse
elisealcala profile image
Elizabeth Alcalá Author

Hi, sorry for the late reply, if you check the repo the configuration to pack it with electron-builder is already there.

Collapse
claudina profile image
claudia d

Just what I need. You are my hero ❤️

Collapse
scrmea profile image
scrmea

Hi, great article, thank you!
I was wondering if babel is really necessary. Couldn't we use the the normal typescript transpiler for that? After all, we don't need to be compatible to different browsers if we ship our own browser.

Collapse
elisealcala profile image
Elizabeth Alcalá Author

Hi, I haven't tried it but I think it could work.

Collapse
madza profile image
Madza

It can do wonders, VS Code as an example :)

Collapse
thedavvy profile image
Davvy

Is it possible to start both react and electron with just one command?

Collapse
whitehatcip profile image
whiteHatCip

hi!
just add the concurrently npm package to the dev dependencies and your scripts should be like this:

"scripts": {
"dev": "concurrently --success first \"npm run dev:electron\" \"npm run dev:react\" -k",
"dev:electron": "cross-env NODE_ENV=development webpack --config webpack.electron.config.js --mode development && electron .",
"dev:react": "cross-env NODE_ENV=development webpack serve --config webpack.react.config.js --mode development && electron ."
},

As you can see, the dev script calls the concurrently command and the commands that you need to execute concurrently have to be put one after the other among quotation marks ("), escaping them, obviously.

Forem Open with the Forem app