electron-redux-multi-window-comm
v1.1.0
Published
Simplify communication between multiple Electron windows
Downloads
1
Readme
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)
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
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:
- editPath
- selectKeys
- filterByValue
- filterKeys
That for example means that filterKeys
runs only on the keys selected by selectKeys
.
Select Root (filter object) (used with selectKeys)
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)
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)
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)
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