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.
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.
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
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.
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.
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.
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
)