/reduxtron

:electron: end-to-end electron state management

Primary LanguageTypeScript

reduxtron hero image

end-to-end electron state management

features

  1. frontend framework agnostic, use whatever you like, even vanilla js.
  2. global state, single-source of truth for all state you want to share.
  3. making the app state as predictable as any redux implementation (actions, devtools, etc)
  4. frontend, tray, main node process dispatch type-defined actions
  5. all above mentioned pieces receive the new state back thru redux subscriptions
  6. single place to write redux middleware with full node.js access (file-system, fetch, db, etc)
  7. easy way to persist and retrieve state (reading/writing to a json file, saving to a local db, fetching/posting to external api, etc)
  8. ipc performance, no api layer, without any manual ipc messaging/handling to write
  9. follows latest electron safety recommendations (sandbox: true + nodeIntegration: false + contextIsolation: true)

why reduxtron

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

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'

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

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.

repo organization

this is a monorepo containing the code for:

  1. the reduxtron library
  2. the reduxtorn demo app
  3. the reduxtron boilerplates:

the reduxtron library

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

demo app

a ever wip demo app to show off some of the features/patterns this approach enables

demo app screenshot

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:

  1. naïve persistance (writing to a json file on every state change + reading it on initialization)

  2. zustand-based store and selectors (to prevent unnecessary rerenders)

  3. swr-like reducer to store data from different sources (currently http + file-system)

  4. 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
  5. 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

boilerplates

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.

why redux?

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:


so why redux was chosen?

  1. framework agnostic, it’s just javascript™ (so it can run on node, browser, or any other js runtime needed) — compared to (recoil, pinia)
  2. single store (compared to mobx, xstate and others)
  3. single "update" function, with a single signature (so it’s trivial to register on the preload and have end-to-end type-safety)
  4. single "subscribe" function to all the state — same as above reasons
  5. can use POJOs as data primitive (easy to serialize/deserialize on inter-process communication)

related projects and inspiration

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.

  • klarna/electron-redux

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