Electron × TypeScript × React × TypeORM

Be my guest at the Electron table, take a seat and order your dishes, I'll be pleased to serve you 🍽️

Published Mar 11, 2019 visitor badge

Tags:  Electron  Webpack  React  TypeORM  TypeScript

At first I had an idea, since I love papertable RPG I would like to build a desktop app built with electron which players can join a game room and follow game activity (such as life points, currency, possessions…) So,let’s get using electron.

Then I thought about the language I wanted to use and my preference for front-end project is Typescript since it’s built with hard types.

At this point I was thinking about how the data of the different clients can besaved and syncronized easily. I recently started to use TypeORM and I found it powerful, easily configurable and it reminds me about Doctrine and Hibernate.

Finally, for the front-end capabilities I choose React (since it’s the trendy one I know) with webpack and HMR.

Starter

The first thing we need to do is configure every component to work isolately then with all others.

Webpack

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

module.exports = {
    // a simple entry point
    entry: ["./src/index.tsx"],
    // configuration for HMR
    devServer: {
        contentBase: path.join(__dirname, "dist"),
        compress: true,
        port: 9000,
    },
    // needed to run correctly all JavaScript especially TypeORM
    target: "electron-renderer",
    module: {
        rules: [
            {
                test: /\.tsx?$/,
                use: "ts-loader",
                exclude: /node_modules/,
            },
        ],
    },
    resolve: {
        extensions: [".tsx", ".ts", ".js"],
    },
    output: {
        filename: "[name].js",
        path: path.resolve(__dirname, "dist"),
    },
    plugins: [
        new webpack.NormalModuleReplacementPlugin(/typeorm$/, function (result) {
            result.request = result.request.replace(/typeorm/, "typeorm/browser");
        }),
        new webpack.ProvidePlugin({
            "window.SQL": "sql.js/js/sql.js",
        }),
        new HtmlWebpackPlugin({
            // this allows us to add a div#app inside the body
            template: "index.html",
        }),
    ],
};

TypeScript

{
  "compilerOptions": {
    "outDir": "./dist/",
    "noImplicitAny": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "module": "commonjs",
    "target": "es5",
    "jsx": "react",
    "allowJs": true,
    "moduleResolution": "node",
    "typeRoots": [
      "node_modules/@types"
    ]
  },
  "exclude": [
    "node_modules"
  ]
}

TypeORM

{
  "type": "sqljs",
  "synchronize": true,
  "autoSave": true,
  "entities": [
    Hello
  ],
  "logging": true,
  "logger": "advanced-console",
  "location": "core-db"
}

package.json (used by electron)

{
  "main": "main.js",
  "scripts": {
    "prestart": "NODE_ENV=production webpack",
    "start": "electron .",
    "dev": "NODE_ENV=development concurrently 'sleep 1 && yarn run dev:electron' 'yarn run dev:webpack'",
    "dev:electron": "electron .",
    "dev:webpack": "webpack-dev-server"
  }
}

Main course

The goal here is to allow a component do display specific content using data fetched from the database.

We need to prepare the database connection that will be used inside a component. That’s why, in the entrypoint defined in webpack we need to create it and then use it inside our components using the getRepository or getConnection methods provided by TypeORM.

// src/index.tsx
import {createConnection} from "typeorm";
import * as ReactDOM from "react-dom";
import * as React from "react";
import "reflect-metadata";
import Hello from "./entities/Hello";
import HelloComponent from "./component/HelloComponent";

createConnection({
    type: "sqljs",
    synchronize: true,
    autoSave: true,
    entities: [Hello],
    logging: true,
    logger: "advanced-console",
    location: "core-db",
}).then(() => {
    ReactDOM.render(<HelloComponent id = {1}
    />, document.getElementById("app"));
});

The first component

// src/component/HelloComponent.ts
import * as React from "react";
import * as PropTypes from "prop-types";

interface HelloProps {
    id: number;
}

export default class HelloComponent extends React.Component<HelloProps> {
    static propTypes = {
        id: PropTypes.number.isRequired,
    };

    state: any = {
        firstname: "",
        lastname: "",
    };

    render() {
        return (
            <div>
                Hello
        {
            this.state.firstname
        }
        {
            this.state.lastname
        }
        </div>
    )
        ;
    }
}

Using the local database

// src/component/HelloComponent.ts
import * as React from "react";
import * as PropTypes from "prop-types";
import {getRepository} from "typeorm";
import Hello from "../entities/Hello";

interface HelloProps {
    id: number;
}

export default class HelloComponent extends React.Component<HelloProps> {
    static propTypes = {
        id: PropTypes.number.isRequired,
    };

    state: any = {
        firstname: "",
        lastname: "",
    };

    async componentDidMount() {
        const entity = await getRepository(Hello).findOne({id: this.props.id});
        this.setState({
            firstname: entity.firstname,
            lastname: entity.lastname,
        });
    }

    render() {
        return (
            <div>
                Hello
        {
            this.state.firstname
        }
        {
            this.state.lastname
        }
        </div>
    )
        ;
    }
}

The electron entrypoint

At last if we want to have hot reload enabled we need to specify at the entrypoint used by electron to use a url or a file.

// main.js
const path = require("path");
const {app, BrowserWindow} = require("electron");

const environment = process.env.NODE_ENV || "production";
let win;

function createWindow() {
    win = new BrowserWindow({});

    if (environment === "development") {
        win.webContents.openDevTools();
        win.loadURL("http://0.0.0.0:9000/");
    } else {
        win.loadFile(path.resolve(__dirname, "dist/index.html"));
    }

    win.on("closed", () => {
        win = null;
    });
}

app.on("ready", createWindow);

app.on("window-all-closed", () => {
    if (process.platform !== "darwin") {
        app.quit();
    }
});

app.on("activate", () => {
    if (win === null) {
        createWindow();
    }
});

Dessert

You can clone or fork this repository if you want to start your own project.

Et voilà! 🍒

Read more