![reduxtron hero image](/vitordino/reduxtron/raw/main/assets/readme-hero-light.png)
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.
![diagram containing a typical react + redux setup](/vitordino/reduxtron/raw/main/assets/readme-diagram-frontend-light.png)
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.
![diagram containing a typical react + redux setup on the left, several electron and node.js specific api on the right, and a vertical line on the middle written 'inter-process'](/vitordino/reduxtron/raw/main/assets/readme-diagram-question-light.png)
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.
![reduxtron setup: with redux, business logic and node/electron api running on the main process and the web frontend on the renderer. all pieces connect to the redux piece using actions and subscriptions](/vitordino/reduxtron/raw/main/assets/readme-diagram-full-light.png)
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 [email protected]: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
)