Managing state on Electron apps

Effortlessly write a performant, cross-process, end-to-end type-safe store for electron on a familiar way

  • electron
  • state
  • redux

It all started when I first had contact with a large desktop application, it had a view layer written with web technologies and a “backend” layer written with an entirely different language — running on users’ computers. It resembled the typical client-server architecture we usually find on web applications: rest-like http endpoints, an SPA frontend, and a somewhat big gap between them.

A diagram showing two blocks ‘interwebs’ on the left side, containing services, and ‘local computer’ on the right side, containing a “backend” layer and a view layer. There’s http requests flowing from view layer to the backend layer, and from both layers to the services inside the ‘interwebs’ block

After a while, I realized that although we mostly mimic what happens on traditional web frameworks we needed some extra power to accomplish what we envisioned. We started breaking standards: http endpoints weren’t enough, so we needed something to sync the two sides. So we wrapped our endpoints on a custom-made web-sockets layer, ideally keeping all the endpoints intact, and magically get updates across the individual parts of the app using web-socket messages.

A diagram showing two blocks ‘interwebs’ on the left side, containing services, and ‘local computer’ on the right side, containing a “backend” layer and a view layer. There’s http requests from both layers to the services inside the ‘interwebs’ block. But now the view layer connects to the “backend” layer using web-socket messages

While it’s a bit magical and served the needs for a considerable amount of time, iterating on a product with so many layers felt a bit too tiresome, error-prone, and mostly inefficient. We ended up using this proprietary communication layer between every software piece, but as there were multiple programming languages, it required:

  • serializing the internal state to json for sending data
  • deserializing the json to a custom struct for consuming data
  • make sure the application was still describable as a non-circular directed graph for coordinating dependencies between these internal pieces

A diagram showing two blocks ‘interwebs’ on the left side, containing services, and ‘local computer’ on the right side, containing a group of plugins as the “backend” layer and a view layer. There’s http requests from each plugin and the view layer to the services inside the ‘interwebs’ block. But now the bottom plugins receive web-socket messages from the plugins above them, and the view layer also requests web-sockets messages from the plugins

I felt that the aforementioned limitations started slowing us down, and the whole system was pretty cumbersome to explain all the gymnastics we put in place to overcome the shortcomings.

We should put redux on the backend

A co-worker once joked

That phrase struck me: only the utterly derranged would dream of putting these words together — but also — what more could we lose? We were still wandering, looking for alternatives, and no obvious one seemed to fit. We even tried following the best practices of API and web development. However, that didn’t solve all our problems, at least not in the long run.

Then, reassessing the phrase, it all made sense: we weren’t building a traditional client/server web application. The “frontend” and “backend” weren’t miles apart: they were the same application! Running on the same computer!

No matter how many programming languages, or how deep the system calls would be, the application was still mostly frontend: a client to some external services.

Of course, a typical html+css+js inside a browser can’t achieve everything a desktop application might want to do. Some features need code outside the browser window, interacting with the system, io, etc. But that doesn’t mean the communication needs to be complex.

Looking for inspiration, most of the resources online on “electron+redux” point to the typical way of using redux on the frontend, either under the react tree or some other frontend framework.

A diagram showing two blocks ‘interwebs’ on the left side, now empty, and ‘local computer’ on the right side, containing a ‘node main process’ and a ‘renderer process’, the latter containing the react logo a plus sign and the redux logo. There are dotted lines with a question mark between the ‘interwebs’ and the ‘local computer’, and another one between the processes inside the ‘local computer’

But I knew there was potential to use it to bridge the node layer (usually called the “main” process) and other parts of the app.

On github and reading some blog posts, I discovered many electron projects also used express.js (or other generic node server library) on the main process. So one idea we could try standardizing redux inside http, but I feel like coming back to the same complexity/indirection road we strive for getting away from.

tutorials

Reading a bit closer to the source — on electron’s documentation — I realized it also exposed this “inter-process communication” API that allows different parts of the application to talk to each other on this 2-way event handling fashion using anything that could be serialized as messages, going through named channels.

The examples are usually to open some native dialog or simple things like console logging inputs from the other side, but it was always boresome to write. On top of that, it didn’t grant end-to-end type-safeness (you need to write it yourself on both processes, what each message on each channel receives and returns).

We had our mind set on some guiding principles:

  • a single source of truth (one redux store)
  • laying on the longer running process (the main node process)
  • a simple way for all the pieces (frontend, tray, node) to subscribe and send actions transparently.

I was lucky to find we weren’t the only crazy ones, it turns out Klarna fiddled with this combination in 2016 and it served as a great source of inspiration. It followed most of the above requirements, but was a bit more complex than I would like: they use a redux store on the main process, and another on each browser window — and they also stopped maintaining it.

reduxtron

It took just a couple of hours to create a proof-of-concept — after all, it’s just typescript, running on both sides. Most of the work was figuring out what we wanted, and the technical part was integrating two well-defined and well-documented APIs.

What’s next?

As usual, I got excited with the idea, and evolved the naïve proof-of-concept as an open-source library called reduxtron: it contains a demo application showing off some cool features and also some streamlined boilerplates focused on react, svelte and vue development.

The core library is rather small and already follows the latest electron safety guidelines, so I don’t expect to need that much maintenance or adding features. As it’s backed by redux™, you can plug your logic, or use libraries like redux-undo)

Related reads: