REDUX ARCHITECTURE IN ELECTRON

If you have experience working with Electron applications, you may be familiar with Redux as a state management tool. Redux is a predictable state container for JavaScript applications that manages a single source of truth for the entire application state. In this post, we will explore how the Redux architecture can be implemented in an Electron application.

alt Types2Json

"Using redux with electron poses a couple of problems. Processes (main and renderer) are completely isolated, and the only mode of communication is IPC"
[source]

If you are facing this problem, your solution may be to use the library electron-redux. However, if for any reason you cannot use that library, or you don't need to have one store for the main and the renderer process, stay and see how we can use it as inspiration to solve this problem.

import { configureStore } from '@reduxjs/toolkit';
import { persistReducer, persistStore } from 'redux-persist';
import createElectronStorage from 'redux-persist-electron-storage';

import rootReducer from './rootReducer';
import forwardToMain from './middlewares/forwardToMain';


const persistConfig = {
  key: 'root',
  storage: createElectronStorage(),
};

const persistedReducer = persistReducer(persistConfig, rootReducer);

const store = configureStore({
  reducer: persistedReducer,
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(forwardToMain, ...othersMiddleware),
})
...

export const persistor = persistStore(store);
export default store;

It is important that our forwardToMain file is the first middleware to execute in order to carry out our flow of events.

import { ipcRenderer } from 'electron';

export const forwardToMain =
  store =>
  next =>
  action => {
    try {
      const serializableAction = structuredClone(action);
      if (serializableAction) {
        if (action.meta && action.meta.scope === 'local') return next(action);
        ipcRenderer.send('redux-action', action);
      }
      return next(action);
    } catch (error) {
      return next(action);
    }
  };

export default forwardToMain;

Once we send all actions to the main process using ipcRenderer.send('redux-action', action), we will be waiting in the main process for this event to be sent to all other windows in our Electron application.

ipcMain.on('redux-action', (_, action) => {
  for (const window of BrowserWindow.getAllWindows()) {
    // the action now includes metadata to prevent an infinite loop of Redux events from occurring:
    window.webContents.send('redux-action', { ...action, meta: { ...action.meta, scope: 'local' } });
  }
});

Then, we need to have a listener in our windows to keep our Redux state synchronized.

  const syncActions = useCallback(
    (_, action) => {
      dispatch(action);
    },
    [dispatch]
  );

  useEffect(() => {
    ipcRenderer.on('redux-action', syncActions);
    return () => {
      ipcRenderer.off('redux-action', syncActions);
    };
  }, [syncActions]);

As a final recommendation, you could add a function that triggers local actions. It may be that you have events that are only important for one window in general and not for all windows. In that case, we could have a helper function like this.

export const useLocalDispatch = () => {
  const dispatch = useDispatch();
  return (action) =>
    dispatch({
      ...action,
      meta: {
        ...action.meta,
        scope: 'local',
      },
    });
};

With this approach, we can keep our store synchronized between all windows of our Electron application.

If you have any questions, comments, or suggestions, you can contact me at @CarlosManotasV.