Build stateful micro frontends by sharing Redux state and actions between <iframe>
modules and container applications:
- Pass Redux actions across window boundaries using the Browser's event system.
- Inherit parts of the Redux state from a parent application when a module is loaded.
- Save Redux state in the Browser's session / local storage and retrieve it on reloading of modules.
Redux-iframe is a tiny 2.5k library with Redux as only dependency.
npm install redux-iframe
or
yarn add redux-iframe
Note: The demo folder contains an application demonstrating several use cases of redux-iframe
.
Define all ducks needed by the application
and its modules in a shared library (say shared
).
// shared/index.js
export const MY_STATE = 'MY_STATE'
export const SET_MY_STATE = 'SET_MY_STATE'
export const setMyState = (payload) => ({
type: SET_MY_STATE,
payload
})
export default (state = {}, action) => (
action.type === SET_MY_STATE ? action.payload : state
)
Actions are passed between modules, their parent application, and vice versa by leveraging the Browser's event system
(postMessage
and addEventListener
). All actions to be passed need to be defined in a shared library to guarantee
correct marshalling/unmarshalling with JSON.stringify
and JSON.parse
.
// module/index.js
import { applyMiddleware, combineReducers, createStore } from 'redux'
import { createParentEventSender } from 'redux-iframe'
import { default as sharedReducer, MY_STATE, SET_MY_STATE } from 'shared'
const reducers = { [MY_STATE]: sharedReducer }
const eventSender = createParentEventSender([SET_MY_STATE])
const store = createStore(combineReducers(reducers), applyMiddleware(eventSender))
store.dispatch({ type: SET_MY_STATE, payload: 'Hello, world!' })
// parent/index.js
import { combineReducers, createStore } from 'redux'
import { installEventListener } from 'redux-iframe'
import { default as sharedReducer, MY_STATE, SET_MY_STATE } from 'shared'
const reducers = { [MY_STATE]: sharedReducer }
const store = createStore(combineReducers(reducers))
installEventListener(store, [SET_MY_STATE])
Analogous to module-to-parent direction, except that instead of createParentEventSender
function createModuleEventSender
should be used.
// parent/index.js
import { createModuleEventSender } from 'redux-iframe'
import { SET_MY_STATE } from 'shared'
const eventSender = createModuleEventSender([SET_MY_STATE], 'iframe-id')
// ...
The second parameter 'iframe-id' refers to the id
attribute of the loaded iframe: <iframe id='iframe-id'>
.
All modules should use the same iframe id. They should filter the actions they are interested in by passing the
corresponding action names as second parameter of function installEventListener
.
Modules can copy parts of the parent application's state on loading. Currently this works only under the following assumptions:
- Parent application and module iframe have the same origin.
- The parent's state is immutable (which is a general requirement for Redux to work properly).
- The state slice to copy is just below the root of the state tree, identified by keys (
[MY_STATE]
in our example).
// parent/index.js
import { combineReducers, createStore } from 'redux'
import { makeStoreGlobal } from 'redux-iframe'
import { default as sharedReducer, MY_STATE } from 'shared'
const store = createStore(combineReducers({ [MY_STATE]: sharedReducer }))
makeStoreGlobal(store)
// module/index.js
import { combineReducers, createStore } from 'redux'
import { getParentState } from 'redux-iframe'
import { default as sharedReducer, MY_STATE } from 'shared'
const reducers = { [MY_STATE]: sharedReducer }
const initialState = getParentState([MY_STATE])
const store = createStore(combineReducers(reducers), initialState)
Modules may save parts of their state in the Browser's session storage (default) or local storage and retrieve it on re-loading. The storage cycle is triggered on each action, but the actual writing to web storage only happens if one of the state parts changed. As for state inheritance, this currently works for top-level keys only.
// module/index.js
import { combineReducers, createStore } from 'redux'
import { getStoredState, installStorageWriter } from 'redux-iframe'
import { default as sharedReducer, MY_STATE } from 'shared'
const reducers = { [MY_STATE]: sharedReducer }
const initialState = getStoredState([MY_STATE])
const store = createStore(combineReducers(reducers), initialState)
installStorageWriter(store, [MY_STATE])
If you want to use local storage (which keeps the state even if the tab or Browser is closed) instead of session storage,
you can provide an additional argument to getStoredState
and installStorageWriter
. Usingh that argument object, you can
also choose a different key name for the state object (default is "redux-iframe-state"):
import { combineReducers, createStore } from 'redux'
import { getStoredState, installStorageWriter, StorageType } from 'redux-iframe'
import { default as sharedReducer, MY_STATE } from 'shared'
const reducers = { [MY_STATE]: sharedReducer }
const initialState = getStoredState([MY_STATE], { storageType: StorageType.LOCAL, rootKey: 'my-key' })
const store = createStore(combineReducers(reducers), initialState)
installStorageWriter(store, [MY_STATE], { storageType: StorageType.LOCAL, rootKey: 'my-key' })
The results of functions getParentState
and getStoredState
can be merged. Function combineState
can merge states with different
or the same top-level keys. If the keys are equal, arguments to the right overwrite left arguments, just as Object.assign()
does.
// module/index.js
import { combineReducers, createStore } from 'redux'
import { combineState, getParentState, getStoredState, installStorageWriter } from 'redux-iframe'
import { default as sharedReducer, MY_STATE } from '../shared'
const reducers = { [MY_STATE]: sharedReducer }
const initialState = combineState(
getStoredState([MY_STATE]),
getParentState([MY_STATE]))
const store = createStore(combineReducers(reducers), initialState)
installStorageWriter(store, [MY_STATE])
In the example, the parent state overwrites the local state, if any.
- Allow inheriting the parent state via
postMessage
/addEventListener
- Allow copies of state slices below the root level
MIT