diff --git a/README.md b/README.md
index fd42c734..7370f56f 100644
--- a/README.md
+++ b/README.md
@@ -35,20 +35,40 @@ electron-redux docs are located at **electron-redux.js.org**. You can find there
## Quick start
-electron-redux comes as a [Redux StoreEnhancer](https://redux.js.org/understanding/thinking-in-redux/glossary#store-enhancer). To initialize your stores, you just need to decorate them in the `main` and `renderer` processes of electron with their respective enhancers:
+### Basic setup
+
+If you have a setup without any enhancers, also including middleware, you can use the basic setup. For the basic setup, electron redux exposes a [Redux StoreEnhancer](https://redux.js.org/understanding/thinking-in-redux/glossary#store-enhancer). You simply add the enhancer to your createStore function to set it up.
```ts
// main.ts
-import { mainStateSyncEnhancer } from 'electron-redux'
+import { stateSyncEnhancer } from 'electron-redux'
-const store = createStore(reducer, mainStateSyncEnhancer())
+const store = createStore(reducer, stateSyncEnhancer())
```
```ts
// renderer.ts
-import { rendererStateSyncEnhancer } from 'electron-redux'
+import { stateSyncEnhancer } from 'electron-redux'
+
+const store = createStore(reducer, stateSyncEnhancer())
+```
+
+### Multi-enhancer setup
+
+> This setup is required when you have other enhancers/middleware. This is especially the case for enhancers or middleware which dispatch actions, such as **redux-saga** and **redux-observable**
+
+For this setup we will use the `composeWithStateSync` function. This function is created to wrap around your enhancers, just like the [compose](https://redux.js.org/api/compose) function from redux. When using this, you will not need `stateSyncEnhancer` as this does the same thing under the hood. If you do, it will throw an error.
+
+```ts
+import { createStore, applyMiddleware, compose } from 'redux'
+import { composeWithStateSync } from 'electron-redux'
+
+const middleware = applyMiddleware(...middleware)
+
+// add other enhances here if you have any, works like `compose` from redux
+const enhancer: StoreEnhancer = composeWithStateSync(middleware /* ... other enhancers ... */)
-const store = createStore(reducer, rendererStateSyncEnhancer())
+const store = createStore(reducer, enhancer)
```
That's it!
diff --git a/docs/docs/faq/faq-general.md b/docs/docs/faq/faq-general.md
index ac671891..0ab2d771 100644
--- a/docs/docs/faq/faq-general.md
+++ b/docs/docs/faq/faq-general.md
@@ -6,3 +6,16 @@ hide_title: true
---
# TODO
+
+## Errors
+
+### Received error "electron-redux has already been attached to a store"
+
+There are 2 scenario's for you to receive this error message.
+
+1. If you are using the `composeWithStateSync` function to install electron-redux, you do not need to manually add the `stateSyncEnhancer` as it does the same thing. It will throw an error if you try.
+2. If you are using `stateSyncEnhancer`, `rendererStateSyncEnhancer` or `mainStateSyncEnhancer` in your createStore function, you may only add one of these in EACH process.
+
+### Received error "Unsupported process: process.type = ..."
+
+If you use `composeWithStateSync` or `stateSyncEnhancer`, we will determine in which process you are, the main or renderer process. We do this by checking the [process.type](https://www.electronjs.org/docs/api/process#processtype-readonly) variable which has been set by Electron. If you receive this error, you are either using this package in a non-supported environment, or this variable is not set properly
diff --git a/docs/docs/introduction/getting-started.md b/docs/docs/introduction/getting-started.md
index fc810f41..bfdf776e 100644
--- a/docs/docs/introduction/getting-started.md
+++ b/docs/docs/introduction/getting-started.md
@@ -23,20 +23,40 @@ npm install electron-redux@alpha
# Configuration
-electron-redux comes as a [Redux store enhancer](https://redux.js.org/understanding/thinking-in-redux/glossary#store-enhancer). To initialize your stores, you just need to decorate them in the `main` and `renderer` processes of electron with their respective enhancers:
+### Basic setup
+
+If you have a setup without any enhancers, also including middleware, you can use the basic setup. For the basic setup, electron redux exposes a [Redux StoreEnhancer](https://redux.js.org/understanding/thinking-in-redux/glossary#store-enhancer). You simply add the enhancer to your createStore function to set it up.
```ts
// main.ts
-import { mainStateSyncEnhancer } from 'electron-redux'
+import { stateSyncEnhancer } from 'electron-redux'
-const store = createStore(reducer, mainStateSyncEnhancer())
+const store = createStore(reducer, stateSyncEnhancer())
```
```ts
// renderer.ts
-import { rendererStateSyncEnhancer } from 'electron-redux'
+import { stateSyncEnhancer } from 'electron-redux'
+
+const store = createStore(reducer, stateSyncEnhancer())
+```
+
+### Multi-enhancer setup
+
+> This setup is required when you have other enhancers/middleware. This is especially the case for enhancers or middleware which dispatch actions, such as **redux-saga** and **redux-observable**
+
+For this setup we will use the `composeWithStateSync` function. This function is created to wrap around your enhancers, just like the [compose](https://redux.js.org/api/compose) function from redux. When using this, you will not need `stateSyncEnhancer` as this does the same thing under the hood. If you do, it will throw an error.
+
+```ts
+import { createStore, applyMiddleware, compose } from 'redux'
+import { composeWithStateSync } from 'electron-redux'
+
+const middleware = applyMiddleware(...middleware)
+
+// add other enhances here if you have any, works like `compose` from redux
+const enhancer: StoreEnhancer = composeWithStateSync(middleware /* ... other enhancers ... */)
-const store = createStore(reducer, rendererStateSyncEnhancer())
+const store = createStore(reducer, enhancer)
```
That's it!
diff --git a/src/composeWithStateSync.ts b/src/composeWithStateSync.ts
new file mode 100644
index 00000000..b512d4c3
--- /dev/null
+++ b/src/composeWithStateSync.ts
@@ -0,0 +1,44 @@
+/* eslint-disable @typescript-eslint/ban-types */
+
+import { StoreEnhancer } from 'redux'
+import { forwardAction } from './forwardAction'
+import { StateSyncOptions } from './options/StateSyncOptions'
+import { stateSyncEnhancer } from './stateSyncEnhancer'
+
+const forwardActionEnhancer = (options?: StateSyncOptions): StoreEnhancer => (createStore) => (
+ reducer,
+ preloadedState
+) => {
+ const store = createStore(reducer, preloadedState)
+
+ return forwardAction(store, options)
+}
+
+const extensionCompose = (options: StateSyncOptions) => (
+ ...funcs: StoreEnhancer[]
+): StoreEnhancer => {
+ return (createStore) => {
+ return [
+ stateSyncEnhancer({ ...options, preventActionReplay: true }),
+ ...funcs,
+ forwardActionEnhancer(options),
+ ].reduceRight((composed, f) => f(composed), createStore)
+ }
+}
+
+export function composeWithStateSync(
+ options: StateSyncOptions
+): (...funcs: Function[]) => StoreEnhancer
+export function composeWithStateSync(...funcs: StoreEnhancer[]): StoreEnhancer
+export function composeWithStateSync(
+ firstFuncOrOpts: StoreEnhancer | StateSyncOptions,
+ ...funcs: StoreEnhancer[]
+): StoreEnhancer | ((...funcs: StoreEnhancer[]) => StoreEnhancer) {
+ if (arguments.length === 0) {
+ return stateSyncEnhancer()
+ }
+ if (arguments.length === 1 && typeof firstFuncOrOpts === 'object') {
+ return extensionCompose(firstFuncOrOpts)
+ }
+ return extensionCompose({})(firstFuncOrOpts as StoreEnhancer, ...funcs)
+}
diff --git a/src/forwardAction.ts b/src/forwardAction.ts
new file mode 100644
index 00000000..5bb73bd9
--- /dev/null
+++ b/src/forwardAction.ts
@@ -0,0 +1,51 @@
+import { ipcRenderer, webContents } from 'electron'
+import { Store } from 'redux'
+import { IPCEvents } from './constants'
+import { MainStateSyncEnhancerOptions } from './options/MainStateSyncEnhancerOptions'
+import { RendererStateSyncEnhancerOptions } from './options/RendererStateSyncEnhancerOptions'
+import { StateSyncOptions } from './options/StateSyncOptions'
+import { isMain, isRenderer, validateAction } from './utils'
+
+export const processActionMain = (
+ action: A,
+ options: MainStateSyncEnhancerOptions = {}
+): void => {
+ if (validateAction(action, options.denyList)) {
+ webContents.getAllWebContents().forEach((contents) => {
+ // Ignore chromium devtools
+ if (contents.getURL().startsWith('devtools://')) return
+ contents.send(IPCEvents.ACTION, action)
+ })
+ }
+}
+
+export const processActionRenderer = (
+ action: A,
+ options: RendererStateSyncEnhancerOptions = {}
+): void => {
+ if (validateAction(action, options.denyList)) {
+ ipcRenderer.send(IPCEvents.ACTION, action)
+ }
+}
+
+export const forwardAction = >(
+ store: S,
+ options?: StateSyncOptions
+): S => {
+ return {
+ ...store,
+ dispatch: (action) => {
+ const value = store.dispatch(action)
+
+ if (!options?.preventActionReplay) {
+ if (isMain) {
+ processActionMain(action, options)
+ } else if (isRenderer) {
+ processActionRenderer(action, options)
+ }
+ }
+
+ return value
+ },
+ }
+}
diff --git a/src/index.ts b/src/index.ts
index a4a98709..23e259f4 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,5 +1,13 @@
import { mainStateSyncEnhancer } from './mainStateSyncEnhancer'
import { stopForwarding } from './utils'
import { rendererStateSyncEnhancer } from './rendererStateSyncEnhancer'
+import { stateSyncEnhancer } from './stateSyncEnhancer'
+import { composeWithStateSync } from './composeWithStateSync'
-export { mainStateSyncEnhancer, rendererStateSyncEnhancer, stopForwarding }
+export {
+ mainStateSyncEnhancer,
+ rendererStateSyncEnhancer,
+ stopForwarding,
+ stateSyncEnhancer,
+ composeWithStateSync,
+}
diff --git a/src/mainStateSyncEnhancer.ts b/src/mainStateSyncEnhancer.ts
index f175f3c5..ef966916 100644
--- a/src/mainStateSyncEnhancer.ts
+++ b/src/mainStateSyncEnhancer.ts
@@ -1,22 +1,21 @@
import { ipcMain, webContents } from 'electron'
-import {
- Action,
- compose,
- Dispatch,
- Middleware,
- MiddlewareAPI,
- StoreCreator,
- StoreEnhancer,
-} from 'redux'
+import { Action, StoreEnhancer } from 'redux'
import { IPCEvents } from './constants'
-import {
- defaultMainOptions,
- MainStateSyncEnhancerOptions,
-} from './options/MainStateSyncEnhancerOptions'
-import { preventDoubleInitialization, stopForwarding, validateAction } from './utils'
+import { forwardAction } from './forwardAction'
+import { MainStateSyncEnhancerOptions } from './options/MainStateSyncEnhancerOptions'
+import { stopForwarding } from './utils'
+
+/**
+ * Creates new instance of main process redux enhancer.
+ * @param {MainStateSyncEnhancerOptions} options Additional enhancer options
+ * @returns StoreEnhancer
+ */
+export const mainStateSyncEnhancer = (
+ options: MainStateSyncEnhancerOptions = {}
+): StoreEnhancer => (createStore) => {
+ return (reducer, preloadedState) => {
+ const store = createStore(reducer, preloadedState)
-function createMiddleware(options: MainStateSyncEnhancerOptions) {
- const middleware: Middleware = (store) => {
ipcMain.handle(IPCEvents.INIT_STATE_ASYNC, async () => {
return JSON.stringify(store.getState(), options.serializer)
})
@@ -28,6 +27,7 @@ function createMiddleware(options: MainStateSyncEnhancerOptions) {
// When receiving an action from a renderer
ipcMain.on(IPCEvents.ACTION, (event, action: Action) => {
const localAction = stopForwarding(action)
+
store.dispatch(localAction)
// Forward it to all of the other renderers
@@ -42,46 +42,6 @@ function createMiddleware(options: MainStateSyncEnhancerOptions) {
})
})
- return (next) => (action) => {
- if (validateAction(action, options.denyList)) {
- webContents.getAllWebContents().forEach((contents) => {
- // Ignore chromium devtools
- if (contents.getURL().startsWith('devtools://')) return
- contents.send(IPCEvents.ACTION, action)
- })
- }
-
- return next(action)
- }
- }
- return middleware
-}
-
-/**
- * Creates new instance of main process redux enhancer.
- * @param {MainStateSyncEnhancerOptions} options Additional enhancer options
- * @returns StoreEnhancer
- */
-export const mainStateSyncEnhancer = (options = defaultMainOptions): StoreEnhancer => (
- createStore: StoreCreator
-) => {
- preventDoubleInitialization()
- const middleware = createMiddleware(options)
- return (reducer, preloadedState) => {
- const store = createStore(reducer, preloadedState)
-
- let dispatch = store.dispatch
-
- const middlewareAPI: MiddlewareAPI> = {
- getState: store.getState,
- dispatch,
- }
-
- dispatch = compose(middleware(middlewareAPI))(dispatch)
-
- return {
- ...store,
- dispatch,
- }
+ return forwardAction(store, options)
}
}
diff --git a/src/options/MainStateSyncEnhancerOptions.ts b/src/options/MainStateSyncEnhancerOptions.ts
index 971bcfce..e3f9f823 100644
--- a/src/options/MainStateSyncEnhancerOptions.ts
+++ b/src/options/MainStateSyncEnhancerOptions.ts
@@ -1,15 +1,10 @@
-export type MainStateSyncEnhancerOptions = {
+import { StateSyncOptions } from './StateSyncOptions'
+
+export interface MainStateSyncEnhancerOptions extends StateSyncOptions {
/**
* Custom store serialization function.
* This function is called for each member of the object. If a member contains nested objects,
* the nested objects are transformed before the parent object is.
*/
serializer?: (this: unknown, key: string, value: unknown) => unknown
-
- /**
- * Custom list for actions that should never replay across stores
- */
- denyList?: RegExp[]
}
-
-export const defaultMainOptions: MainStateSyncEnhancerOptions = {}
diff --git a/src/options/RendererStateSyncEnhancerOptions.ts b/src/options/RendererStateSyncEnhancerOptions.ts
index 270d49c1..f6160b08 100644
--- a/src/options/RendererStateSyncEnhancerOptions.ts
+++ b/src/options/RendererStateSyncEnhancerOptions.ts
@@ -1,4 +1,6 @@
-export type RendererStateSyncEnhancerOptions = {
+import { StateSyncOptions } from './StateSyncOptions'
+
+export interface RendererStateSyncEnhancerOptions extends StateSyncOptions {
/**
* Custom function used during de-serialization of the redux store to transform the object.
* This function is called for each member of the object. If a member contains nested objects,
@@ -6,11 +8,6 @@ export type RendererStateSyncEnhancerOptions = {
*/
deserializer?: (this: unknown, key: string, value: unknown) => unknown
- /**
- * Custom list for actions that should never replay across stores
- */
- denyList?: RegExp[]
-
/**
* By default, the renderer store is initialized from the main store synchronously.
* Since the synchronous fetching of the state is blocking the renderer process until it gets the state
@@ -19,5 +16,3 @@ export type RendererStateSyncEnhancerOptions = {
*/
lazyInit?: boolean
}
-
-export const defaultRendererOptions: RendererStateSyncEnhancerOptions = {}
diff --git a/src/options/StateSyncOptions.ts b/src/options/StateSyncOptions.ts
new file mode 100644
index 00000000..57d016ba
--- /dev/null
+++ b/src/options/StateSyncOptions.ts
@@ -0,0 +1,11 @@
+export interface StateSyncOptions {
+ /**
+ * Custom list for actions that should never replay across stores
+ */
+ denyList?: RegExp[]
+
+ /**
+ * Prevent replaying actions in the current process
+ */
+ preventActionReplay?: boolean
+}
diff --git a/src/rendererStateSyncEnhancer.ts b/src/rendererStateSyncEnhancer.ts
index 0c6df751..402cd522 100644
--- a/src/rendererStateSyncEnhancer.ts
+++ b/src/rendererStateSyncEnhancer.ts
@@ -1,36 +1,11 @@
import { ipcRenderer } from 'electron'
-import {
- Action,
- compose,
- Dispatch,
- Middleware,
- MiddlewareAPI,
- StoreCreator,
- StoreEnhancer,
-} from 'redux'
+import { Action, StoreEnhancer } from 'redux'
import { IPCEvents } from './constants'
+import { forwardAction } from './forwardAction'
import { fetchInitialState, fetchInitialStateAsync } from './fetchState'
import { replaceState, withStoreReplacer } from './fetchState/replaceState'
-import {
- defaultRendererOptions,
- RendererStateSyncEnhancerOptions,
-} from './options/RendererStateSyncEnhancerOptions'
-import { preventDoubleInitialization, stopForwarding, validateAction } from './utils'
-
-const createMiddleware = (options: RendererStateSyncEnhancerOptions): Middleware => (store) => {
- // When receiving an action from main
- ipcRenderer.on(IPCEvents.ACTION, (_, action: Action) => {
- store.dispatch(stopForwarding(action))
- })
-
- return (next) => (action) => {
- if (validateAction(action, options.denyList)) {
- ipcRenderer.send(IPCEvents.ACTION, action)
- }
-
- return next(action)
- }
-}
+import { RendererStateSyncEnhancerOptions } from './options/RendererStateSyncEnhancerOptions'
+import { stopForwarding } from './utils'
/**
* Creates new instance of renderer process redux enhancer.
@@ -39,15 +14,12 @@ const createMiddleware = (options: RendererStateSyncEnhancerOptions): Middleware
* @param {RendererStateSyncEnhancerOptions} options Additional settings for enhancer
* @returns StoreEnhancer
*/
-export const rendererStateSyncEnhancer = (options = defaultRendererOptions): StoreEnhancer => (
- createStore: StoreCreator
-) => {
- preventDoubleInitialization()
-
+export const rendererStateSyncEnhancer = (
+ options: RendererStateSyncEnhancerOptions = {}
+): StoreEnhancer => (createStore) => {
return (reducer, state) => {
- const middleware = createMiddleware(options)
-
const initialState = options.lazyInit ? state : fetchInitialState(options)
+
const store = createStore(
options.lazyInit ? withStoreReplacer(reducer) : reducer,
initialState
@@ -59,18 +31,11 @@ export const rendererStateSyncEnhancer = (options = defaultRendererOptions): Sto
})
}
- let dispatch = store.dispatch
-
- const middlewareAPI: MiddlewareAPI> = {
- getState: store.getState,
- dispatch,
- }
-
- dispatch = compose(middleware(middlewareAPI))(dispatch)
+ // When receiving an action from main
+ ipcRenderer.on(IPCEvents.ACTION, (_, action: Action) => {
+ store.dispatch(stopForwarding(action))
+ })
- return {
- ...store,
- dispatch,
- }
+ return forwardAction(store, options)
}
}
diff --git a/src/stateSyncEnhancer.ts b/src/stateSyncEnhancer.ts
new file mode 100644
index 00000000..b8f89bd9
--- /dev/null
+++ b/src/stateSyncEnhancer.ts
@@ -0,0 +1,17 @@
+import { StoreEnhancer } from 'redux'
+import { mainStateSyncEnhancer } from './mainStateSyncEnhancer'
+import { StateSyncOptions } from './options/StateSyncOptions'
+import { rendererStateSyncEnhancer } from './rendererStateSyncEnhancer'
+import { isMain, isRenderer, preventDoubleInitialization } from './utils'
+
+export const stateSyncEnhancer = (config: StateSyncOptions = {}): StoreEnhancer => {
+ preventDoubleInitialization()
+
+ if (isRenderer) {
+ return rendererStateSyncEnhancer(config)
+ } else if (isMain) {
+ return mainStateSyncEnhancer(config)
+ }
+
+ throw new Error(`Unsupported process: process.type = ${process?.type}`)
+}
diff --git a/src/utils/actions.ts b/src/utils/actions.ts
index fcbe2615..d6c75c19 100644
--- a/src/utils/actions.ts
+++ b/src/utils/actions.ts
@@ -1,4 +1,4 @@
-import { isFSA, FluxStandardAction } from './isFSA'
+import { FluxStandardAction, isFSA } from './isFSA'
// Gives us just enough action type info to work for the functions below
export type ActionMeta = {
@@ -9,7 +9,7 @@ export type ActionMeta = {
* stopForwarding allows you to give it an action, and it will return an
* equivalent action that will only play in the current process
*/
-export const stopForwarding = (action: FluxStandardAction) => ({
+export const stopForwarding = (action: FluxStandardAction): any => ({
...action,
meta: {
...action.meta,
diff --git a/src/utils/misc.ts b/src/utils/misc.ts
index 8fa915b3..2b75a2b6 100644
--- a/src/utils/misc.ts
+++ b/src/utils/misc.ts
@@ -31,3 +31,6 @@ export const trimProperties = (props: T[], obj: X) => {
Object.entries(obj).filter(([key]) => !props.includes(key as T))
) as Omit
}
+
+export const isRenderer = process.type === 'renderer'
+export const isMain = process.type === 'browser'
diff --git a/tests/typescript/composeWithStateSync.ts b/tests/typescript/composeWithStateSync.ts
new file mode 100644
index 00000000..ea517e7c
--- /dev/null
+++ b/tests/typescript/composeWithStateSync.ts
@@ -0,0 +1,24 @@
+import { composeWithStateSync } from '../../types'
+import { applyMiddleware, createStore, Store, StoreEnhancer } from 'redux'
+import { reducer, CounterState, Actions } from '../counter'
+import { countMiddleware } from '../middleware'
+
+// This is just a dummy enhancer, this does nothing
+const someOtherEnhancer: StoreEnhancer = (next) => {
+ return (reducer, state) => {
+ return next(reducer, state)
+ }
+}
+
+const middleware = applyMiddleware(countMiddleware)
+
+const enhancerWithoutOptions: StoreEnhancer = composeWithStateSync(middleware, someOtherEnhancer)
+
+const store: Store = createStore(reducer, enhancerWithoutOptions)
+
+const enhancerWithOptions: StoreEnhancer = composeWithStateSync({ denyList: [] })(
+ middleware,
+ someOtherEnhancer
+)
+
+const store2: Store = createStore(reducer, enhancerWithOptions)