Jack Stephens
Jack Stephens' Latest Blog

Jack Stephens' Latest Blog

FrakenApp #1: Part 7, React

Jack Stephens's photo
Jack Stephens
·Mar 7, 2022·

4 min read

Table of contents

After getting Webpack and Electron to play nicely together in the last post, getting React up and running was easy. Too easy, in fact. I could create a .jsx file and render it with ReactDOM.render(), recreate many examples from various tutorials from across the web in my Electron app, etc.

Things went sideways for me when I tried to receive Inter-Process Communication (IPC) messages from the main Electron process into my React component. It was all in my mind; I needed a shift in perspective. I had trouble visualizing where to put the two ipcRenderer calls.

I use final state of the previous article as the starting point.

Getting React and Electron to play nicely together

Putting Messages Received into a State variable

In the earlier versions of the Phone Home App, I would embed any messages I received through ipcRenderer.on('received-data' into the DOM. The full code for Dialog.jsx is here

This time I put this data into a state variable, along with a couple of other variables that I needed to keep track of other parts of the app.

class Dialog extends Component {
  constructor(props) {
    super(props);
    this.state = {message: "", returnedMessages: [], maxMessageId: 0};

where:

  • message: bound to the text box
  • returnedMessages: the actual list
  • maxMessageId: the ID of the last message

Working with ipcRenderer in Dialog.jsx

After much experimentation, I could make ipcRenderer calls inside the React component! Sending is easy; I call ipcRenderer.send() from within the event function.

The hard part is getting messages back from the Main process; where do I call ipcRenderer.on()? I ended up calling ipcRenderer.once() after the send call. once() is like on() except that the listener is automatically removed after it is invoked a single time, which works well in this case because I don't need to clean up after I receive the one message I expect.

So the code looks like this:

At the top of the .jsx file I added:

const ipcRenderer = require('electron').ipcRenderer;

I made all of the calls to ipcRenderer in the SendMessage() method:

// This logic is used in 2 places
SendMessage (messageValue) {
  if (messageValue) {
    // Send the message home, the Main process will send it on
    ipcRenderer.send('phoneHome', messageValue);
    // This does not survive the closure
    const myComponent = this;
    // Get the response back from the Main process
    // once recieves a single message, it is like a one time on(). 
    ipcRenderer.once('received-data', function (evt, message) {
      // Add this item to returnedMessages, React will rerender the UI
      const id = myComponent.state.maxMessageId + 1;
      const newMessages = [...myComponent.state.returnedMessages, 
        {id, content: message}];
      myComponent.setState({returnedMessages: newMessages});
      myComponent.setState({maxMessageId: id});
      // Clear the phoneHome textBox
      myComponent.setState({message: ""});
    });  
  }
}

Transpiling the .jsx file(s)

React uses Babel to translate the JSX files into plain JavaScript. So I need to add a rule to tell Webpack to use the babel-loader on all .jsx files. To do this, I added the following to the rules array in my webpack.config.js file:

{
  test: /\.jsx?$/,
  loader: 'babel-loader'
}

So now Webpack knows what to do. I also need to tell Babel what to do by adding a new file called babel.config.js

module.exports = {
  presets:[
      "@babel/preset-env",
      "@babel/preset-react"
  ]
}

Cutting renderer.ts down to size

This file got smaller. All I do is call ReactDOM.render(). I use React.createElement(Dialog) instead of '` because the latter wouldn't work in TypeScript, and I didn't want to spend the time to figure out what's wrong.

import Dialog from './components/Dialog.jsx';
import React  from 'react';
import ReactDOM from 'react-dom';

ReactDOM.render(
  // Couldn't get "<React />" to work with TypeScript
  React.createElement(Dialog),
  document.getElementById('app-content')
);

And cutting index.ejs down too

All I need is a div with the id of app-content for React to find.

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <title>Phone Home App</title>
</head>

<body>
    <h1>Phone Home App</h1>
    <br />
    <div id="app-content"></div>
</body>

</html>

Added Dev Dependencies

the Phone Home App has the same dependencies as the last post plus these:

  • @babel/core
  • @babel/preset-env
  • @babel/preset-react
  • @types/react
  • @types/react-dom
  • fs
  • path
  • react
  • react-dom

Progress in the FrakenApp #1

What I have

  • The app uses some TypeScript
  • Still using Webpack
  • Still have the .NET Core part.

What's still missing

  • No Oracle.
  • Backsliding on TypeScript, used a .jsx file.
  • Not building the Electron App; I'm running it from VS Code.
  • Didn't test it on the Mac this cycle

The Code

The code is available on my GitHub as Frakenapp-01 or the Release for this article

 
Share this