2019-03-20

How to migrate ReactXP App to make an Electron Desktop App

ReactXP comes with a few sample apps. The TodoList app is the most developed and closest to the structure of a production app written in TypeScript, but doesn't target Electron.  Here's how to migrate it to run on Electron so it could work as a desktop app.

I'm on a Mac, but Electron is cross platform with Windows and Linux so it should work there too.  ReactXP 1.6.x is current right now (hilariously, it upgraded from 1.5.x just as I was working on this and it introduced some new error so this migration is totally in beta).

Strategically, the idea is ReactXP can already directly target the web via React (in addition to native iOS, Android, and Windows via React Native).  So we just target the web with ReactXP, modify the web page loader to run in Electron, and modify some code so it doesn't assume it's running at the root of a web server.

Make TodoList into a web app. Run it on a local dev web server

First, use git to clone the ReactXP repository:

git clone https://github.com/microsoft/reactxp

In the reactxp/samples directory, you'll find the TodoList app. Open it:

cd reactxp/samples/TodoList/

Install Electron into it:

npm install --save-dev electron

Now we need a web page to load the app into Electron.  We'll use the default one from Electron quick start.  Just copy the main.js from electron-quick-start into the TodoList folder, and rename it to electron-main.js just for clarity.


Edit electron-main.js and change the function call

mainWindow.loadFile('index.html')

to

mainWindow.loadURL("http://localhost:8080")

That way it'll load the ReactXP web app from the development server that npm starts later.

Modify the "main" entry in package.json to point to "electron-main.js", i.e. change it to:

"main": "electron-main.js"

Also add a "scripts" entry in package.json to start Electron from command line easily:

"scripts": {
...
    "electron": "electron .",
...
}

Now we're ready to compile the TypeScript code:

npm run start-web

After running that once, if you make changes to the files in the src folder but not package.json, you can recompile your TypeScript code more quickly by just running:

npm run gulp-web

(See the package.json scripts to find out why this is faster than the start-web command.)

The TypeScript app is now compiled into JavaScript in the web folder.  Gulp will keep running to recompile any modified TypeScript files.

To server the web app in a local dev web server, open a new terminal in TodoList folder and run:

node nodeserver.js

Now to load the app into Electron, open yet another new terminal window in TodoList and run:

npm run electron

This method gives you quick development turnaround time as gulp will recompile your TypeScript code, the node server serves it live, and Electron and reload it live.  But in production, you probably wouldn't ship your app this way.  It'd need to be compiled to static files loaded directly by Electron.


Production build: static files, relative paths

In production build, the static HTML file in TodoList/web/index.html is used.  Just modify electron-main.js to load that instead: i.e. replace the function call to

mainWindow.loadURL("http://localhost:8080")

with:

mainWindow.loadFile('web/index.html')

Unfortunately, by default, React builds uses absolute paths as if your web app is running from the root directory.  This is true of ReactXP as well.  So the app will fail to load in Electron as the app obviously can't be expected to sit in the root directory of your PC.

To fix this, we have to fix web/index.html (which appears to be hand-built rather than compiled in this sample app), fix some TypeScript code, and fix package.json to configure React to use relative paths.

Open web/index.html and change every href and src attribute to use relative instead of absolute paths.  This means replacing every

href="/

with (note the dot)

href="./

And also replace every

src="/

with (note the dot again)

src="./

Then open TodoList/src/ts/app/AppConfig.ts and modify the getDocRoot() function to return

return window.require('electron').remote.app.getAppPath()+'/web/';

so that any TypeScript code using that function will instead get an absolute path that is relative to the TodoList/web folder, wherever TodoList happens to be in the file system.

Then open package.json and add a new "homepage" property to point to the current directory.  I added it right after "main", which we've modified previously:

"main": "electron-main.js",
"homepage":".",

Note the "homepage" property value is a single dot and not "./" (as I had originally thought).  Apparently that makes a difference, and is an official thing in React (specifically react-scripts) [1].

Then you can compile as before by running

npm run start-web

or

npm run gulp-web

There is no more need to run the node local web server.  Just run electron to run the app:

npm run electron

If you really want to use the development server, it's possible to write more code to make it easier switching between using the local server for development, and the static page for production builds (see [2]).  I prefer to use the production static page even during development though.


Future work: production build bug fixes

Bug 1 (FIXED!):
This was fixed above!  Namely editing TodoList/src/ts/app/AppConfig.ts and modify the getDocRoot() function to return

return window.require('electron').remote.app.getAppPath()+'/web/';

instead of

return './'; // or '.'

so that any TypeScript code using that function will instead get an absolute path that is relative to the TodoList/web folder, wherever TodoList happens to be in the file system.

There are still absolute paths showing up for some reason.  These errors present in the dev console when the window is resized and the view is refreshed:


ViewBase.js:115 GET file:///images/todo-logo.png net::ERR_FILE_NOT_FOUND
ViewBase.js:115 GET file:///images/todo-small.png net::ERR_FILE_NOT_FOUND

And this error present when a new todo item is created and saved (thus switching between screens):


react-dom.development.js:3048 GET file:///images/todo-small.png net::ERR_FILE_NOT_FOUND


Bug 2 (FIXED!):
This was fixed in ReactXP as of 2019 Apr 6 with the latest version of ReactXP and its reactxp-virtuallistview 2.0.0.

I was about to publish this, then ReactXP 1.6.x landed, and I'm getting this new error when compiling/building with gulp:

ERROR in /Users/user_home/dev/reactxp/samples/TodoList/src/ts/views/TodoListPanel.tsx
./src/ts/views/TodoListPanel.tsx
[tsl] ERROR in /Users/user_home/dev/reactxp/samples/TodoList/src/ts/views/TodoListPanel.tsx(129,18)
      TS2322: Type '{ itemList: TodoListItemInfo[]; renderItem: (details: VirtualListCellRenderDetails<TodoListItemInfo>) => Element; style: StyleRuleSet<ViewStyle>; }' is not assignable to type 'IntrinsicAttributes & IntrinsicClassAttributes<VirtualListView<TodoListItemInfo>> & Readonly<VirtualListViewProps<TodoListItemInfo>> & Readonly<{ children?: ReactNode; }>'.
  Property 'style' does not exist on type 'IntrinsicAttributes & IntrinsicClassAttributes<VirtualListView<TodoListItemInfo>> & Readonly<VirtualListViewProps<TodoListItemInfo>> & Readonly<{ children?: ReactNode; }>'.

But this bug is independent of the migration we did above and as far as I can tell, the app still compiles and runs fine (other than bug 1 above).


References

I learned from many others, but oddly most only write on one aspect or another but don't put the whole thing together from top to bottom like I did above, and especially nothing quite so comprehensively showing how to get a ReactXP app to run in Electron.  Thanks to the following though!

[1] See also:
     facebook/create-react-app: Is there a way to remove first "/" from resources url? #1094
     webpack-contrib/file-loader: How to deal with relative public path #46
     Serving the Same Build from Different Paths.

[2] How to build an Electron app using create-react-app. No webpack configuration or “ejecting” necessary

[3] ReactXP Getting Started: Building Your First ReactXP App

[4] vicentedealencar/reactxp-electron

[5] electron/electron-quick-start

[6] How to build a Desktop Application with Electron and React

No comments: