Electron Adventures: Episode 14: React
Tags: electron, javascript, react
Published: 2021-08-07 10:52:45

In previous episode, I showed how to setup Electron project with Svelte + rollup frontend. This time we'll do the same with React + webpack.

And again, we'll do it by creating a React app first, and connecting Electron to it as a second step; not the other way around.

### Create a React app

We start the usual way, by creating a new React app, and removing all the crap we don't need.

In fact the default template contains so much crap we don't need, [I'm going to use another template](https://github.com/rdilweb/cra-template-ready).

```shell
$ npx create-react-app episode-14-react --use-npm --template ready
```

If you're into React, you might have a favorite template already, and you can use it instead. Pretty much all of them will work with Electron just fine.

### Disable browser auto-open

We need to do one thing. React has annoying habit of opening browser window when you start - but we're not doing a browser app!

So edit `package.json` and replace `start` line with:

```javascript:
"start": "BROWSER=none react-scripts start",
```

### Add Electron

As before, no special steps needed here:

```shell
$ npm i --save-dev electron
```

### Add backend script `index.js`

We can take existing file, just point it at our dev server. When we package the app we'll need to make it aware of which environment it's in, and to point at that URL, or at the generated file, based on that.

The only difference from Svelte version is the default port number.

```javascript
let { app, BrowserWindow } = require("electron")

function createWindow() {
  let win = new BrowserWindow({
    webPreferences: {
      preload: `${__dirname}/preload.js`,
    },
  })
  win.maximize()
  win.loadURL("http://localhost:3000/")
}

app.on("ready", createWindow)

app.on("window-all-closed", () => {
  app.quit()
})
```

### Add preload script `preload.js`

We don't need to do any changes, so taking it directly from the previous episode:

```javascript
let child_process = require("child_process")
let { contextBridge } = require("electron")

let runCommand = (command) => {
  return child_process.execSync(command).toString().trim()
}

contextBridge.exposeInMainWorld(
  "api", { runCommand }
)
```

### Customize `public/index.html` and `src/index.js`

I'm only going to change the title, the ones coming from the template are good enough as they are.

### `src/index.css`

Svelte has scoped CSS builtin, so I used that. React has packages for that as well, but since it's not builtin, I'll just use global CSS file here, pretty much the same we used before in episode 10.

Here's its contents:

```css
body {
  background-color: #444;
  color: #fff;
  font-family: monospace;
}

.input-line {
  display: flex;
  gap: 0.5rem;
}

.input-line > * {
  flex: 1;
}

.input-line > .prompt {
  flex: 0;
}

.output {
  padding-bottom: 0.5rem;
}

.input {
  color: #ffa;
}

.output {
  color: #afa;
  white-space: pre;
}

form {
  display: flex;
}

input {
  flex: 1;
  font-family: inherit;
  background-color: inherit;
  color: inherit;
  border: none;
}
```

### Main component `src/App.js`

We just import two components and use them. For simplicity, command state will be handled by `CommandInput` component, I did not export it here.

The app uses `window.api.runCommand` which was created by the preload script before it started. As `runCommand` is synchronous, it can really mess our React app. We'll fix that in a later episode.

```javascript
import React from "react"
import CommandInput from "./CommandInput"
import HistoryEntry from "./HistoryEntry"

export default (props) => {
  let [history, setHistory] = React.useState([])

  let onsubmit = (command) => {
    let output = window.api.runCommand(command)
    setHistory([...history, { command, output }])
  }

  return (
    <>
      <h1>React Terminal App</h1>
      { history.map(({command, output}, index) => (
        <HistoryEntry key={index} command={command} output={output} />
      ))}
      <CommandInput onsubmit={onsubmit} />
    </>
  )
}
```

### History entry component `src/HistoryEntry.js`

It's completely passive, just displays two passed props:

```javascript
import React from "react"

export default ({command, output}) => {
  return <>
    <div className='input-line'>
      <span className='prompt'>$</span>
      <span className='input'>{command}</span>
    </div>
    <div className='output'>{output}</div>
  </>
}
```

### Command input component `src/CommandInput.js`

It keeps command in a local state, and only callbacks when the user submits.

```javascript
import React from "react"

export default ({ onsubmit }) => {
  let [command, setCommand] = React.useState("")

  let submit = (e) => {
    e.preventDefault()
    onsubmit(command)
    setCommand("")
  }

  return <div className="input-line">
    <span className="prompt">$</span>
    <form onSubmit={submit}>
      <input type="text" autoFocus value={command} onChange={(e) => setCommand(e.target.value)} />
    </form>
  </div >
}
```

### Result

And here's the result:

![Episode 14 screenshot](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5udu9xdyshe3mfgbwkir.png)

This wasn't any harder than the Svelte version. Pretty much every real world React app uses long list of extra React addons like redux, immer, styled-components, and so on, and most of them work just fine with Electron, so customize to your heart's content.

In the next episode, we'll make our backend async, so a slow command won't freeze the whole frontend.

As usual, [all the code for the episode is here](https://github.com/taw/electron-adventures/tree/master/episode-14-react).