Electron Adventures: Episode 14: React
Tomasz Wegrzanowski
Posted on August 7, 2021
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.
$ 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:
"start": "BROWSER=none react-scripts start",
Add Electron
As before, no special steps needed here:
$ 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.
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:
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:
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.
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:
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.
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:
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.
Posted on August 7, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
June 9, 2020