Simplify communication between multiple Electron windows
JavaScript
Switch branches/tags
Nothing to show
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
src
test
.babelrc
.gitignore
LICENSE.md
README.md
actions.js
constants.js
package.json
selectors.js

README.md

electron-redux-multi-window-comm

The aim of this library is to simplify communication between multiple Electron windows.

Important: At the moment you have to keep you reducer state in Immutable.js and use combineReducers from redux for the top reducers in order for this library to work.

Getting Started

Installation

npm install electron-redux-multi-window-comm --save

Usage

electron-redux-multi-window-comm is built on top of redux-saga, redux and Immutable.js

Important: The ElectronReduxCommEnhancer has to be the first argument of compose.

redux-saga up to 0.9.x

import {
	createStore,
	applyMiddleware,
	combineReducers,
	compose,
} from 'redux';
import createSagaMiddleware from 'redux-saga';

import {
	ElectronReduxCommSaga,
	ElectronReduxCommReducer,
	ElectronReduxCommEnhancer
} from 'electron-redux-multi-window-comm';

import AppReducer from 'path/to/reducer'

const store = createStore(
	combineReducers({
			'ElectronReduxComm': ElectronReduxCommReducer,
			'App': AppReducer,
			// ...
		}
	),
	compose(
		ElectronReduxCommEnhancer({
			windowName: 'MyWindow',
			subscribeTo: [
				{
					windowName: 'MyOtherWindow',
					stateFilter: {
						App: {
							clickCounter: true,
						}
					},
					actionTypes: [
						'ACTION_1',
						'ACTION_2',
					]
				}
			]
		}),
		applyMiddleware(createSagaMiddleware(ElectronReduxCommSaga/*, ...*/)),
		// DevTools.instrument()
	)
);

redux-saga since 0.10.x

import {
	createStore,
	applyMiddleware,
	combineReducers,
	compose,
} from 'redux';
import createSagaMiddleware from 'redux-saga';

import {
	ElectronReduxCommSaga,
	ElectronReduxCommReducer,
	ElectronReduxCommEnhancer
} from 'electron-redux-multi-window-comm';

import AppReducer from 'path/to/reducer'

const sagaMiddleware = createSagaMiddleware()

const store = createStore(
	combineReducers({
			'ElectronReduxComm': ElectronReduxCommReducer,
			'App': AppReducer,
			// ...
		}
	),
	compose(
		ElectronReduxCommEnhancer({
			windowName: 'MyWindow',
			subscribeTo: [
				{
					windowName: 'MyOtherWindow',
					stateFilter: {
						App: {
							clickCounter: true,
						}
					},
					actionTypes: [
						'ACTION_1',
						'ACTION_2',
					]
				}
			]
		}),
		applyMiddleware(sagaMiddleware)),
		// DevTools.instrument()
	)
);

sagaMiddleware.run(ElectronReduxCommSaga)

Test app

You can see electron-redux-multi-window-comm in action in the test app, which demonstrates simple use of this library.

Debugger

There is also a simple debugger, that shows current window subscribers and subscriptions and also several last sent and received Global Actions.

Global Action

Global Action solves the problem when you need to tell something to another window. There is no need to setup ipc channels or perhaps a websocket connection by yourself. Instead dispatch a Global Action the same way as any other action and it will be sent to the desired window.

Global Action is a higher-order action that wraps another action and is then sent to its target window.

Here we see an example of dispatching an acton AN_ACTION to a targetWindow.

import {makeGlobalAction} from 'electron-redux-multi-window-comm/actions';

store.dispatch(makeGlobalAction({type: 'AN_ACTION'}, 'targetWindow'))

store.dispatch here can also be this.props.dispatch in connected component or any other way you use to dispatch actions to redux

Action Subscription

Action Subscription solves the problem when you have for example a button that dispatches an action on click and you want that action to be both dispatched locally and also sent to another window. Instead of editing the click handler and adding a dispatch of a Global Action you use Action Subscription.

Or perhaps you want to know how many times a todo item was added in another window. With Action Subscription you do not need to change any code in the todo window, instead you simply subscribe to ADD_TODO action and that's it.

By subscribing to actions from another window you will receive them all whenever they are dispatched in the target window.

Action Subscriptions are also stored locally so if you subscribe to actions from window that't doesn't exist yet it will receive the subscription after its creation.

Subscription via enhancer options

subscribeTo: [
  {
    windowName: 'targetWindow',
    actionTypes: ['ACTION_TYPE_1', 'ACTION_TYPE_2'],
  }
]

Dynamic subscription

import {
  subscribeToWindowActions,
  unsubscribeFromWindowActions,
} from 'electron-redux-multi-window-comm/actions';

store.dispatch(subscribeToWindowActions('targetWindow', ['ACTION_TYPE_1', 'ACTION_TYPE_2']))

// Now you're subscribed to ACTION_TYPE_1 and ACTION_TYPE_2

// Notice the **true**
store.dispatch(subscribeToWindowActions('targetWindow', ['ACTION_TYPE_3'], true))

// Now you're subscribed to ACTION_TYPE_1, ACTION_TYPE_2 and ACTION_TYPE_3

// Notice the absence of **true**
store.dispatch(subscribeToWindowActions('targetWindow', ['ACTION_TYPE_4']))

// Now you're subscribed to ACTION_TYPE_4

store.dispatch(unsubscribeFromWindowActions('targetWindow'))

// Now you're NOT subscribed to any actions

State Subscription

State Subscription solve the problem when you want some part of another window's state, but you don't want to manage receiving all the relevant actions and you also don't want to duplicate all the needed reducers.

By using State Subscription you declare what parts of state of another window you want and you will get it. And also whenever that state changes it will be updated locally, too.

Subscription via enhancer options

subscribeTo: [
  {
    windowName: 'targetWindow',
    stateFilter: {
      App: {
        clickCount: true
      }
    }
  }
]

Dynamic subscription

import {
  subscribeToWindowState,
  unsubscribeFromWindowState,
} from 'electron-redux-multi-window-comm/actions';

let filter = {
  App: {
    clickCount: true
  }
}

store.dispatch(subscribeToWindowState('targetWindow', filter))
store.dispatch(unsubscribeFromWindowState('targetWindow'))

Enhancer

  • windowName (required) - Name of the current window
  • reducerName (optional) - Name used for the lib's reducer in combineReducers (default is ElectronReduxComm)
  • subscribeTo (optional) - Array of subscriptions
  • debug
    • enabled (optional) - Enables storing last x Global Actions, so they can be displayed for example by Debugger
    • numOfActionsToStore (optional) - Number of Global Actions to store

Important: The ElectronReduxCommEnhancer has to be the first argument of compose.

compose(
	ElectronReduxCommEnhancer({
		windowName: 'ourWindow',
		debug: {
			enabled: true,
			numOfActionsToStore: 20,
		},
		subscribeTo: [
			{
				windowName : 'anotherWindow',
				stateFilter: {
					
					App: {
						clickCounter: true
					}
				},
				actionTypes: ['ACTION1', 'ACTION2']
			},
			{
				windowName : 'yetAnotherWindow',
				actionTypes: ['ACTION3']
			}
		]
	}),
    applyMiddleware(createSagaMiddleware(...RootSaga), thunk),
)

State Filter

Important: Try the live playground which also contains several state filter examples.

State filters can be quite complex, if you have need for more advanced filtering options check the tests folder until there is a better documentation here.

Take whole subtree (true)

Live example

const stateFilter = {
  // The top level key always selects the reducer from combinReducers
  App: {
    counters: {
      clickCounter: true
    }
  }
}

You can use false to don't include that key (same as not writing it down in the first place)

Whether clickCounter contains a number, array or object, it will be copied to the filtered state:

App: {
  counters: {
    clickCounter: 8, // [8, 5] or {count: 5} or anything else
  }
}

Edit key path (string)

const stateFilter = {
  // The top level key always selects the reducer from combineReducers
  App: {
    counters: {
      clickCounter: 'myOtherWindowClickCounter'
    }
  }
}

Edit path is similar to using true so that it takes the whole subtree, but the resulting object will have the path modified.

App: {
  counters: {
    myOtherWindowClickCounter: 8, // [8, 5] or {count: 5} or anything else
  }
}

Select keys (array)

Important: Keep in mind that js coerces object properties to string, but Immutable.js doesn't. See Immutable.js readme or this issue

Live example

Imagine this as the state of the App reducer:

App: {
  result: [1, 2],
  selectedArticle: 1,
  entities: {
    articles: {
      1: {
        id: 1,
        title: 'Some Article',
        author: 1
      },
      2: {
        id: 2,
        title: 'Other Article',
        author: 2
      }
    },
    users: {
      1: {
        id: 1,
        name: 'Dan'
      },
      2: {
        id: 2,
        name: 'Frank'
      }
    }
  }
}

This is based on normalizr example

Example 1

const stateFilter = {
  // The top level key always selects the reducer from combinReducers
  App: {
    entities: {
      articles: ['result']
    }
  }
}

The path is always relative to the reducer, so you don't have to write ['App', 'result']. But otherwise you have to specify the whole path ['entities', 'articles']

This will get all the keys from articles that are present in result array

App: {
  entities: {
    articles: {
      1: {
        id: 1,
        title: 'Some Article',
        author: 1
      },
      2: {
        id: 2,
        title: 'Other Article',
        author: 1
      }
    }
  }
}

Example 2

const stateFilter = {
  // The top level key always selects the reducer from combinReducers
  App: {
    entities: {
      articles: ['selectedArticle']
    }
  }
}

This will get only the selectedArticle with key 1.

{
  entities: {
    articles: {
      1: {
        id: 1,
        title: 'Some Article',
        author: 1
      }
    }
  }
}

Example 3

You can also nest the selectors. Here we are getting the author id from the selected article.

const stateFilter = {
  // The top level key always selects the reducer from combinReducers
  App: {
    entities: {
      users: ['entities', 'articles', ['selectedArticle'], 'author']
    }
  }
}

This will first get the selectedArticle id and then use it to get the article's author id and finally the user with that id.

App: {
  entities: {
    users: {
      1: {
        id: 1,
        name: 'Dan'
      }
    }
  }
}

Example 4

Get only the first article and its user

const stateFilter = {
  // The top level key always selects the reducer from combinReducers
  App: {
    entities: {
      articles: ['result', 0],
      users: ['entities', 'articles', ['result', 0], 'author']
    }
  }
}

This will first get the selectedArticle id and then use it to get the article's author id and finally the user with that id.

App: {
  entities: {
    articles: {
      1: {
        id: 1,
        title: 'Some Article',
        author: 1
      }
    },
    users: {
      1: {
        id: 1,
        name: 'Dan'
      }
    }
  }
}

Example 5

Or get only the first two articles and its users

const stateFilter = {
  // The top level key always selects the reducer from combinReducers
  App: {
    entities: {
      articles: ['result', [0, 2]],
      users: ['entities', 'articles', ['result', [0, 2]], 'author']
    }
  }
}

Here we are using the same syntax to slice the result array as in sliceList property of filter object.

App: {
  entities: {
    articles: {
      1: {
        id: 1,
        title: 'Some Article',
        author: 1
      },
      2: {
        id: 2,
        title: 'Other Article',
        author: 2
      }
    },
    users: {
      1: {
        id: 1,
        name: 'Dan'
      },
      2: {
        id: 2,
        name: 'Frank'
      }
    }
  }
}

Filter object

In situations when you need to use multiple operations sucha as select keys and edit path, you need to use filter object

import {
	SHAPE_FILTER_KEY
} from 'electron-redux-multi-window-comm/constants';

{
  [SHAPE_FILTER_KEY] : true,
  editPath  : 'newName',
  sliceList:  [0, 1]
  selectRoot: ['root', 'path'],
  selectKeys: ['path', 'to', 'list', 'or', 'key'],
  filterByValue: {
    key: ['path', 'to', 'value'],
  },
  filterKeys: ['key1', 'key2'],
}

Setting [SHAPE_FILTER_KEY] to false will discard the whole subtree

The order of these operations is the same as in the example above:

  1. editPath
  2. selectKeys
  3. filterByValue
  4. filterKeys

That for example means that filterKeys runs only on the keys selected by selectKeys.

Select Root (filter object) (used with selectKeys)

Live example

Since selectKeys needs the full path it can become tedious to write repeatedly for nested selectors.

Say your articles reducer is deeply nested

App: {
  nested1: {
    nested2: {
      result: [1, 2],
      selectedArticle: 1,
      entities: {
        articles: {
          1: {
            id: 1,
            title: 'Some Article',
            author: 1
          },
          2: {
            id: 2,
            title: 'Other Article',
            author: 2
          }
        },
        users: {
          1: {
            id: 1,
            name: 'Dan'
          },
          2: {
            id: 2,
            name: 'Frank'
          }
        }
      }
    }
  }
}

Now the full path is long and repeated twice

const stateFilter = {
  // The top level key always selects the reducer from combinReducers
  App: {
    nested1: {
      nested2: {
        entities: {
          users: ['nested1', 'nested2', 'entities', 'articles', ['nested1', 'nested2', 'selectedArticle'], 'author']
        }
      }
    }
  }
}

We can simplify it by using selectRoot

const stateFilter = {
  // The top level key always selects the reducer from combinReducers
  App: {
    nested1: {
      nested2: {
        entities: {
          users: {
            [SHAPE_FILTER_KEY]: true,
            selectRoot: ['nested1', 'nested2'],
            selectKeys: ['entities', 'articles', ['selectedArticle'], 'author']
          }
        }
      }
    }
  }
}

The selectRoot is now prepended to every nested selector.

Filter by value (filter object)

Live example

Filters map or list based on given values that are in the form of selectors same as in selectKeys.

You can have multiple selectors on an object and all of the selectors have to match in order for the object to not be filtered out.

App: {
  result: [1, 2],
  selectedArticle: 1,
  selectedUser: 2,
  entities: {
    articles: {
      1: {
        id: 1,
        title: 'Some Article',
        author: 1
      },
      2: {
        id: 2,
        title: 'Other Article',
        author: 2
      }
    },
      '3': {
        id    : '3',
        title : 'Yet Another Article',
        author: '2'
      },
    users: {
      1: {
        id: 1,
        name: 'Dan'
      },
      2: {
        id: 2,
        name: 'Frank'
      }
    }
  }
}

This is based on normalizr example

Here we select only articles that have the same author as selectedUser

const stateFilter = {
  // The top level key always selects the reducer from combinReducers
  App: {
    entities: {
      articles: {
        [SHAPE_FILTER_KEY] : true,
        filterByValue: {
          author: ['entities', 'users', ['selectedUser'], 'id'],
        }
      }
    }
  }
}

First the selector are evaulated, in this case it results to author = 2. Then we filter the object and test whether it has key author and if it equals 2.

App: {
  entities: {
    articles: {
      2: {
        id: 2,
        title: 'Other Article',
        author: 2
      }
    },
      '3': {
        id    : '3',
        title : 'Yet Another Article',
        author: '2'
      },
}

Filter keys (filter object)

Live example

Keeps only selected properties of an object or an array of objects

Runs .map() on the object/array and .filter()s the given keys

const stateFilter = {
  // The top level key always selects the reducer from combinReducers
  App: {
    entities: {
      users: {
        [SHAPE_FILTER_KEY] : true,
        filterKeys: ['title'],
      }
    }
  }
}

This will give us:

App: {
  entities: {
    articles: {
      1: {
        title: 'Some Article',
      },
      2: {
        title: 'Other Article',
      },
    }
  }
}

Or if the articles was an array of objects:

App: {
  entities: {
    articles: [
      {
        title: 'Some Article',
      },
      {
        title: 'Other Article',
      },
    ]
  }
}

Slice List (filter object)

Live example

Applies .slice() on the list with given arguments.

State:

App: {
  list1: [1, 2, 3, 4, 5],
  list2: [1, 2, 3, 4, 5],
  list3: [1, 2, 3, 4, 5],
}

Filter:

const stateFilter = {
  // The top level key always selects the reducer from combinReducers
  App: {
    list1: {
        [SHAPE_FILTER_KEY] : true,
        sliceList: [1],
    },
    list2: {
        [SHAPE_FILTER_KEY] : true,
        sliceList: [-1],
    },
    list3: {
        [SHAPE_FILTER_KEY] : true,
        sliceList: [1, 4],
    },
  }
}

Filtered state:

App: {
  list1: [2, 3, 4, 5],
  list2: [5],
  list3: [2, 3, 4],
}

License

MIT