Skip to content

Commit cce8018

Browse files
sneljo1m-malkowski
andauthored
fix(#285): add composeWithStateSync to resolve issues with enhancer order (#296)
* fix(#285): add composeWithStateSync to resolve issues with enhancer order * fix: resolve PR comments * chore: add type tests * docs: update README getting started and add changes to FAQ Co-authored-by: Maciej Małkowski <monkey3310@gmail.com>
1 parent 134d290 commit cce8018

15 files changed

Lines changed: 260 additions & 134 deletions

README.md

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,20 +35,40 @@ electron-redux docs are located at **electron-redux.js.org**. You can find there
3535

3636
## Quick start
3737

38-
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:
38+
### Basic setup
39+
40+
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.
3941

4042
```ts
4143
// main.ts
42-
import { mainStateSyncEnhancer } from 'electron-redux'
44+
import { stateSyncEnhancer } from 'electron-redux'
4345

44-
const store = createStore(reducer, mainStateSyncEnhancer())
46+
const store = createStore(reducer, stateSyncEnhancer())
4547
```
4648

4749
```ts
4850
// renderer.ts
49-
import { rendererStateSyncEnhancer } from 'electron-redux'
51+
import { stateSyncEnhancer } from 'electron-redux'
52+
53+
const store = createStore(reducer, stateSyncEnhancer())
54+
```
55+
56+
### Multi-enhancer setup
57+
58+
> 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**
59+
60+
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.
61+
62+
```ts
63+
import { createStore, applyMiddleware, compose } from 'redux'
64+
import { composeWithStateSync } from 'electron-redux'
65+
66+
const middleware = applyMiddleware(...middleware)
67+
68+
// add other enhances here if you have any, works like `compose` from redux
69+
const enhancer: StoreEnhancer = composeWithStateSync(middleware /* ... other enhancers ... */)
5070

51-
const store = createStore(reducer, rendererStateSyncEnhancer())
71+
const store = createStore(reducer, enhancer)
5272
```
5373

5474
That's it!

docs/docs/faq/faq-general.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,16 @@ hide_title: true
66
---
77

88
# TODO
9+
10+
## Errors
11+
12+
### Received error "electron-redux has already been attached to a store"
13+
14+
There are 2 scenario's for you to receive this error message.
15+
16+
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.
17+
2. If you are using `stateSyncEnhancer`, `rendererStateSyncEnhancer` or `mainStateSyncEnhancer` in your createStore function, you may only add one of these in EACH process.
18+
19+
### Received error "Unsupported process: process.type = ..."
20+
21+
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

docs/docs/introduction/getting-started.md

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,40 @@ npm install electron-redux@alpha
2323

2424
# Configuration
2525

26-
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:
26+
### Basic setup
27+
28+
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.
2729

2830
```ts
2931
// main.ts
30-
import { mainStateSyncEnhancer } from 'electron-redux'
32+
import { stateSyncEnhancer } from 'electron-redux'
3133

32-
const store = createStore(reducer, mainStateSyncEnhancer())
34+
const store = createStore(reducer, stateSyncEnhancer())
3335
```
3436

3537
```ts
3638
// renderer.ts
37-
import { rendererStateSyncEnhancer } from 'electron-redux'
39+
import { stateSyncEnhancer } from 'electron-redux'
40+
41+
const store = createStore(reducer, stateSyncEnhancer())
42+
```
43+
44+
### Multi-enhancer setup
45+
46+
> 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**
47+
48+
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.
49+
50+
```ts
51+
import { createStore, applyMiddleware, compose } from 'redux'
52+
import { composeWithStateSync } from 'electron-redux'
53+
54+
const middleware = applyMiddleware(...middleware)
55+
56+
// add other enhances here if you have any, works like `compose` from redux
57+
const enhancer: StoreEnhancer = composeWithStateSync(middleware /* ... other enhancers ... */)
3858

39-
const store = createStore(reducer, rendererStateSyncEnhancer())
59+
const store = createStore(reducer, enhancer)
4060
```
4161

4262
That's it!

src/composeWithStateSync.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/* eslint-disable @typescript-eslint/ban-types */
2+
3+
import { StoreEnhancer } from 'redux'
4+
import { forwardAction } from './forwardAction'
5+
import { StateSyncOptions } from './options/StateSyncOptions'
6+
import { stateSyncEnhancer } from './stateSyncEnhancer'
7+
8+
const forwardActionEnhancer = (options?: StateSyncOptions): StoreEnhancer => (createStore) => (
9+
reducer,
10+
preloadedState
11+
) => {
12+
const store = createStore(reducer, preloadedState)
13+
14+
return forwardAction(store, options)
15+
}
16+
17+
const extensionCompose = (options: StateSyncOptions) => (
18+
...funcs: StoreEnhancer[]
19+
): StoreEnhancer => {
20+
return (createStore) => {
21+
return [
22+
stateSyncEnhancer({ ...options, preventActionReplay: true }),
23+
...funcs,
24+
forwardActionEnhancer(options),
25+
].reduceRight((composed, f) => f(composed), createStore)
26+
}
27+
}
28+
29+
export function composeWithStateSync(
30+
options: StateSyncOptions
31+
): (...funcs: Function[]) => StoreEnhancer
32+
export function composeWithStateSync(...funcs: StoreEnhancer[]): StoreEnhancer
33+
export function composeWithStateSync(
34+
firstFuncOrOpts: StoreEnhancer | StateSyncOptions,
35+
...funcs: StoreEnhancer[]
36+
): StoreEnhancer | ((...funcs: StoreEnhancer[]) => StoreEnhancer) {
37+
if (arguments.length === 0) {
38+
return stateSyncEnhancer()
39+
}
40+
if (arguments.length === 1 && typeof firstFuncOrOpts === 'object') {
41+
return extensionCompose(firstFuncOrOpts)
42+
}
43+
return extensionCompose({})(firstFuncOrOpts as StoreEnhancer, ...funcs)
44+
}

src/forwardAction.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { ipcRenderer, webContents } from 'electron'
2+
import { Store } from 'redux'
3+
import { IPCEvents } from './constants'
4+
import { MainStateSyncEnhancerOptions } from './options/MainStateSyncEnhancerOptions'
5+
import { RendererStateSyncEnhancerOptions } from './options/RendererStateSyncEnhancerOptions'
6+
import { StateSyncOptions } from './options/StateSyncOptions'
7+
import { isMain, isRenderer, validateAction } from './utils'
8+
9+
export const processActionMain = <A>(
10+
action: A,
11+
options: MainStateSyncEnhancerOptions = {}
12+
): void => {
13+
if (validateAction(action, options.denyList)) {
14+
webContents.getAllWebContents().forEach((contents) => {
15+
// Ignore chromium devtools
16+
if (contents.getURL().startsWith('devtools://')) return
17+
contents.send(IPCEvents.ACTION, action)
18+
})
19+
}
20+
}
21+
22+
export const processActionRenderer = <A>(
23+
action: A,
24+
options: RendererStateSyncEnhancerOptions = {}
25+
): void => {
26+
if (validateAction(action, options.denyList)) {
27+
ipcRenderer.send(IPCEvents.ACTION, action)
28+
}
29+
}
30+
31+
export const forwardAction = <S extends Store<any, any>>(
32+
store: S,
33+
options?: StateSyncOptions
34+
): S => {
35+
return {
36+
...store,
37+
dispatch: (action) => {
38+
const value = store.dispatch(action)
39+
40+
if (!options?.preventActionReplay) {
41+
if (isMain) {
42+
processActionMain(action, options)
43+
} else if (isRenderer) {
44+
processActionRenderer(action, options)
45+
}
46+
}
47+
48+
return value
49+
},
50+
}
51+
}

src/index.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
import { mainStateSyncEnhancer } from './mainStateSyncEnhancer'
22
import { stopForwarding } from './utils'
33
import { rendererStateSyncEnhancer } from './rendererStateSyncEnhancer'
4+
import { stateSyncEnhancer } from './stateSyncEnhancer'
5+
import { composeWithStateSync } from './composeWithStateSync'
46

5-
export { mainStateSyncEnhancer, rendererStateSyncEnhancer, stopForwarding }
7+
export {
8+
mainStateSyncEnhancer,
9+
rendererStateSyncEnhancer,
10+
stopForwarding,
11+
stateSyncEnhancer,
12+
composeWithStateSync,
13+
}

src/mainStateSyncEnhancer.ts

Lines changed: 17 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,21 @@
11
import { ipcMain, webContents } from 'electron'
2-
import {
3-
Action,
4-
compose,
5-
Dispatch,
6-
Middleware,
7-
MiddlewareAPI,
8-
StoreCreator,
9-
StoreEnhancer,
10-
} from 'redux'
2+
import { Action, StoreEnhancer } from 'redux'
113
import { IPCEvents } from './constants'
12-
import {
13-
defaultMainOptions,
14-
MainStateSyncEnhancerOptions,
15-
} from './options/MainStateSyncEnhancerOptions'
16-
import { preventDoubleInitialization, stopForwarding, validateAction } from './utils'
4+
import { forwardAction } from './forwardAction'
5+
import { MainStateSyncEnhancerOptions } from './options/MainStateSyncEnhancerOptions'
6+
import { stopForwarding } from './utils'
7+
8+
/**
9+
* Creates new instance of main process redux enhancer.
10+
* @param {MainStateSyncEnhancerOptions} options Additional enhancer options
11+
* @returns StoreEnhancer
12+
*/
13+
export const mainStateSyncEnhancer = (
14+
options: MainStateSyncEnhancerOptions = {}
15+
): StoreEnhancer => (createStore) => {
16+
return (reducer, preloadedState) => {
17+
const store = createStore(reducer, preloadedState)
1718

18-
function createMiddleware(options: MainStateSyncEnhancerOptions) {
19-
const middleware: Middleware = (store) => {
2019
ipcMain.handle(IPCEvents.INIT_STATE_ASYNC, async () => {
2120
return JSON.stringify(store.getState(), options.serializer)
2221
})
@@ -28,6 +27,7 @@ function createMiddleware(options: MainStateSyncEnhancerOptions) {
2827
// When receiving an action from a renderer
2928
ipcMain.on(IPCEvents.ACTION, (event, action: Action) => {
3029
const localAction = stopForwarding(action)
30+
3131
store.dispatch(localAction)
3232

3333
// Forward it to all of the other renderers
@@ -42,46 +42,6 @@ function createMiddleware(options: MainStateSyncEnhancerOptions) {
4242
})
4343
})
4444

45-
return (next) => (action) => {
46-
if (validateAction(action, options.denyList)) {
47-
webContents.getAllWebContents().forEach((contents) => {
48-
// Ignore chromium devtools
49-
if (contents.getURL().startsWith('devtools://')) return
50-
contents.send(IPCEvents.ACTION, action)
51-
})
52-
}
53-
54-
return next(action)
55-
}
56-
}
57-
return middleware
58-
}
59-
60-
/**
61-
* Creates new instance of main process redux enhancer.
62-
* @param {MainStateSyncEnhancerOptions} options Additional enhancer options
63-
* @returns StoreEnhancer
64-
*/
65-
export const mainStateSyncEnhancer = (options = defaultMainOptions): StoreEnhancer => (
66-
createStore: StoreCreator
67-
) => {
68-
preventDoubleInitialization()
69-
const middleware = createMiddleware(options)
70-
return (reducer, preloadedState) => {
71-
const store = createStore(reducer, preloadedState)
72-
73-
let dispatch = store.dispatch
74-
75-
const middlewareAPI: MiddlewareAPI<Dispatch<any>> = {
76-
getState: store.getState,
77-
dispatch,
78-
}
79-
80-
dispatch = compose<Dispatch>(middleware(middlewareAPI))(dispatch)
81-
82-
return {
83-
...store,
84-
dispatch,
85-
}
45+
return forwardAction(store, options)
8646
}
8747
}
Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,10 @@
1-
export type MainStateSyncEnhancerOptions = {
1+
import { StateSyncOptions } from './StateSyncOptions'
2+
3+
export interface MainStateSyncEnhancerOptions extends StateSyncOptions {
24
/**
35
* Custom store serialization function.
46
* This function is called for each member of the object. If a member contains nested objects,
57
* the nested objects are transformed before the parent object is.
68
*/
79
serializer?: (this: unknown, key: string, value: unknown) => unknown
8-
9-
/**
10-
* Custom list for actions that should never replay across stores
11-
*/
12-
denyList?: RegExp[]
1310
}
14-
15-
export const defaultMainOptions: MainStateSyncEnhancerOptions = {}
Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,13 @@
1-
export type RendererStateSyncEnhancerOptions = {
1+
import { StateSyncOptions } from './StateSyncOptions'
2+
3+
export interface RendererStateSyncEnhancerOptions extends StateSyncOptions {
24
/**
35
* Custom function used during de-serialization of the redux store to transform the object.
46
* This function is called for each member of the object. If a member contains nested objects,
57
* the nested objects are transformed before the parent object is.
68
*/
79
deserializer?: (this: unknown, key: string, value: unknown) => unknown
810

9-
/**
10-
* Custom list for actions that should never replay across stores
11-
*/
12-
denyList?: RegExp[]
13-
1411
/**
1512
* By default, the renderer store is initialized from the main store synchronously.
1613
* Since the synchronous fetching of the state is blocking the renderer process until it gets the state
@@ -19,5 +16,3 @@ export type RendererStateSyncEnhancerOptions = {
1916
*/
2017
lazyInit?: boolean
2118
}
22-
23-
export const defaultRendererOptions: RendererStateSyncEnhancerOptions = {}

src/options/StateSyncOptions.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export interface StateSyncOptions {
2+
/**
3+
* Custom list for actions that should never replay across stores
4+
*/
5+
denyList?: RegExp[]
6+
7+
/**
8+
* Prevent replaying actions in the current process
9+
*/
10+
preventActionReplay?: boolean
11+
}

0 commit comments

Comments
 (0)