end-to-end electron state management
- frontend framework agnostic, use whatever you like, even vanilla js.
- global state, single-source of truth for all state you want to share.
- making the app state as predictable as any redux implementation (actions, devtools, etc)
- frontend, tray, main node process dispatch type-defined actions
- all above mentioned pieces receive the new state back thru redux subscriptions
- single place to write redux middleware with full node.js access (file-system, fetch, db, etc)
- easy way to persist and retrieve state (reading/writing to a json file, saving to a local db, fetching/posting to external api, etc)
- ipc performance, no api layer, without any manual ipc messaging/handling to write
- follows latest electron safety recommendations (
sandbox: true
+nodeIntegration: false
+contextIsolation: true
)
the average redux setup on web applications (and plenty of tutorials for redux on electron) follows the simpler rule: keeping redux constrained to frontend.
this ends up being pretty limiting because once you want to go past anything broader than a single window/tab of a default web application. there’s no clear or definitive way to share state or communicate between electron layers.
that’s why reduxtron exist, it moves your state "one level up" on the three, to outside your frontend boundary into the broader electron main
process.
with this setup you can both have a single state across all your electron app, without relying on a single browser view and also leverage the full potential of the electron and node APIs, without explicitly writing a single inter-process communication
message.
the premise is simple: every piece of your app can communicate using the same redux™ way (using actions, subscriptions, and getState calls) to a single store.
this is a monorepo containing the code for:
- the reduxtron library
- the reduxtorn demo app
- the reduxtron boilerplates:
set of utilities available on npm to plug into existing electron projects
on your terminal
# install as a regular dependency
npm i reduxtron
create your redux reducers somewhere where both main and renderer processes can import
(for example purposes we’ll be considering a shared/reducers
file).
remember to export your State
and Action
types
initialize your redux store on the main process (we’ll be considering a main/store
for this)
add the following lines onto your main
process entry file:
import { app, ipcMain } from "electron";
import { mainReduxBridge } from "reduxtron/main";
import { store } from "shared/store";
const { unsubscribe } = mainReduxBridge(ipcMain, store);
app.on("quit", unsubscribe);
and this onto your preload
entry file:
import { contextBridge, ipcRenderer } from "electron";
import { preloadReduxBridge } from "reduxtron/preload";
import type { State, Action } from "shared/reducers";
const { handlers } = preloadReduxBridge<State, Action>(ipcRenderer);
contextBridge.exposeInMainWorld("reduxtron", handlers);
this will populate a reduxtron
object on your frontend runtime containing the 3 main redux store functions (inside the global
/window
/globalThis
object):
// typical redux getState function, have the State return type defined as return
global.reduxtron.getState(): State
// typical redux dispatch function, have the Action type defined as parameter
global.reduxtron.dispatch(action: Action): void
// receives a callback that get’s called on each store update
// returns a `unsubscribe` function, you can optionally call it when closing window or when you don’t want to listen for changes anymore.
global.reduxtron.subscribe(callback: ((newState: State) => void) => () => void)
ps: the reduxtron
key here is just an example, you can use any object key you prefer
a ever
wip
demo app to show off some of the features/patterns this approach enables
git clone git@github.com:vitordino/reduxtron.git # clone this repo
cd reduxtron # change directory to inside the repo
npm i # install dependencies
turbo demo # start demo app on development mode
the demo contains some nice (wip) features:
-
naïve persistance (writing to a json file on every state change + reading it on initialization)
-
zustand-based store and selectors (to prevent unnecessary rerenders)
-
swr-like reducer to store data from different sources (currently http + file-system)
-
micro-apps inside the demo:
- a simple to do list with small additions (eg.: external windows to add items backed by different frontend frameworks)
- a dog breed picker (to show off integration with http APIs)
- a finder-like file explorer
-
all the above micro-apps also have a native tray interface, always up-to-date, reads from the same state and dispatches the same actions
as aforementioned, this repo contains some (non-exhaustive, really simple) starters.
currently they are all based on electron-vite, only implements a counter, with a single renderer window and tray to interact with.
spoiler: i’m not a die hard fan of redux nowadays
redux definitely helped a bunch of the early-mid 2010’s web applications. back then, we didn’t had that much nicer APIs to handle a bunch of state for us.
we now have way more tooling for the most common (and maybe worse) use-cases for redux:
-
data-fetching (and caching):
- client-only: swr, react-query, react-router loaders
- or even integrated server-side solutions (like react server components, remix
-
global app state:
so why redux was chosen?
- framework agnostic, it’s just javascript™ (so it can run on node, browser, or any other js runtime needed) — compared to (recoil, pinia)
- single store (compared to mobx, xstate and others)
- single "update" function, with a single signature (so it’s trivial to register on the
preload
and have end-to-end type-safety) - single "subscribe" function to all the state — same as above reasons
- can use POJOs as data primitive (easy to serialize/deserialize on inter-process communication)
while developing this repo, i also searched for what was out there™ in this regard, and was happy to see i wasn’t the only thinking on these crazy thoughts.
-
- belongs to a major company, high visibility
- started around 2016, but stopped being maintained around mid-2020
- had another redux store on the frontend, and sync between them: a bit more complex than i’d like.
- incompatible with electron versions >= 14
-
zoubingwu/electron-shared-state
- individual-led, still relatively maintained
- no redux, single function export
- doesn’t respect electron safety recommendations (needs
nodeIntegration: true
+contextIsolation: false
)