[WIP] redux-worker
React bindings for Redux, where Redux is placed at Web Worker.
Main features
-
⚡️ Always responsive UIAll business logic (reducers, sagas, selectors) is placed at worker thread, so the main thread isn't blocked by those opererations and is freed for UI.
-
⏲ Handling of long-runnning tasksIf worker thread isn't responding to the main thread for certain amount time, the
TASK_DURATION_TIMEOUT
event is fired. The worker thread can be then terminated or rebooted. No need for page reload. -
🐌 At least limited access to thewindow
object in worker contextIf you need access the window object in worker thread, you can use
executeInWindow
async. method, e.g.:
const innerHeight = await executeInWindow('innerHeight')
;
Knowledge requirements
You should known React and Redux. But you don't need to know anything about Web Workers.
Current Limitations
- Import files (container selectors in our case) with
require.context
is really easy, but really slow. Particularly, when we need to search for those files also innode_modules
. This may be solved with a Webpack plugin.
Table of contents
Installing
Using yarn:
$ yarn add @ackee/redux-worker
Using npm:
$ npm i -S @ackee/redux-worker
Core Concepts
TODO
Usage
redux-worker
to your project
Implement -
Add
worker-plugin
to your Webpack configuration. -
Create filed called
getSelectors.js
with the code below:export default function getContainerSelectors() { // import all files that match the regex pattern (e.g. 'Counter.selector.js') // The path must also include node_modules (if any of them uses `connectWorker` HOC) // require.context(pathToRoot, deepLookUp, regexPattern) return require.context('../../../', true, /\.selector\.js$/); }
-
Create file called
configureStore.js
with function that returns the Redux Store:import { createStore } from 'redux'; const rootReducer = (state, action) => state; export default function configureStore() { // The simplest Redux store. return createStore(rootReducer); }
-
Create file called
Store.worker.js
with the code below:import { configureStoreWorker } from '@ackee/redux-worker/worker'; import configureStore from './createStore'; import getSelectors from './getSelectors'; configureStoreWorker({ configureStore, getSelectors, });
-
Create file called
configureReduxWorker.js
with the code below:import * as ReduxWorker from '@ackee/redux-worker/main'; const createStoreWorker = () => { // NOTE: Path to the web worker we've created earlier. return new Worker('./Store.worker.js', { type: 'module', name: 'Store.worker', // optional, for easier debugging }); }; ReduxWorker.initialize(createStoreWorker);
Examples
connectWorker
HOC
Connecting to Redux Store with -
Create new bridge ID for this container:
// --------------------------------------- // modules/counter/constants/index.js // --------------------------------------- import { uniqueId } from '@ackee/redux-worker'; export const bridgeIds = { COUNTER_BRIDGE: uniqueId('COUNTER_BRIDGE'), };
-
Create container component with that ID:
// --------------------------------------- // modules/counter/containers/Counter.js // --------------------------------------- import { connectWorker } from '@ackee/redux-worker/main'; import { bridgeIds } from '../constants'; import Counter from '../components/Counter'; const mapDispatchToProps = dispatch => ({ // ... }); export default connectWorker(bridgeIds.COUNTER_BRIDGE, mapDispatchToProps)(Counter);
-
Create special file for
mapStateToProps
and use that ID:// --------------------------------------- // containers/Counter.selector.js // --------------------------------------- import { registerSelector } from '@ackee/redux-worker/worker'; import { bridgeIds } from '../constants'; const mapStateToProps = state => { return { // ... }; }; registerSelector(bridgeIds.COUNTER_BRIDGE, mapStateToProps);
Rebooting unresponding store worker
// ---------------------------------------
// config/redux-worker/index.js
// ---------------------------------------
import * as ReduxWorker from '@ackee/redux-worker/main';
const createStoreWorker = () => {
return new Worker('./Store.worker.js', {
type: 'module',
name: 'Store.worker',
});
};
ReduxWorker.on(ReduxWorker.eventTypes.TASK_DURATION_TIMEOUT, async () => {
// worker is terminated, then immediately booted again, new redux store is created
await ReduxWorker.storeWorker.reboot();
});
ReduxWorker.initialize(createStoreWorker, {
taskDurationWatcher: {
enabled: true,
},
});
API - Window context
async initialize(createStoreWorker, [config]): void
What does it do:
- Store worker is booted (Redux Store is created).
- An configuration object is created and sent to the worker.
- The task duration watcher is started.
- The window bridge is initialized (see
executeInWindow
section).
Parameters
-
createStoreWorker: Function
- Function that returns new store worker. -
config: Object
- Optional, with following defaults:{ /* const logLevelEnum = { development: logLevels.INFO, production: logLevels.SILENT, [undefined]: logLevels.ERROR, }; */ logLevel: logLevelEnum[process.env.NODE], taskDurationWatcher: { enabled: process.env.NODE_ENV !== 'development', /* If the store worker doesn't report itself in 4s to the main thread, then the worker is considered to be non-responding and the ReduxWorker.eventTypes.TASK_DURATION_TIMEOUT event is fired. */ unrespondingTimeout: 1000 * 4, // ms /* How often should the store worker report itself to the tasksDurationWatcher module. Note that each report resets the unrespondingTimeout. So reportStatusInPeriod + messageHopDelay < unrespondingTimeout. */ reportStatusInPeriod: 1000 * 3, // ms } }
Example
// ...
ReduxWorker.initialize(createStoreWorker).then(() => {
console.log('Store worker is ready');
});
Notes
- This method can be called only once, otherwise an error is thrown.
- The
connectWorker
HOC can be safely used before this method is called. But since the store worker isn't ready at that moment, nothing will be rendered and all incoming messages to the worker are going to be queued.
connectWorker(bridgeId, mapDispatchToProps?, ownPropsSelector?): (ConnectedComponent, Loader?): React.Component
What does it do:
- It connects to Redux store which is placed at a Web Worker. This happens on
componentDidMount
. - It disconnects from Redux store on
componentWillUnmount
.
Parameters
-
bridgeId: String
- An unique ID among other instances ofconnectWorker
. See helper utilityuniqueId
-
mapDispatchToProps: Function|Object
-
Function
: 1st argument is dispatch method that send Redux actions to the store worker. 2nd argument isownProps
object which is returned fromownPropSelector
. -
Object
: Properties are Redux action creators.
-
-
ownPropsSelector: Function
- Function that receives all component props and returns props that are only required in themapDispatchToProps
andmapStateToProps
(e.g.userId
).
Returns
A React component wrapped by connectWorker
HOC.
Example
// --------------------
// Foo.js (main thread context)
// --------------------
import { connectWorker } from '@ackee/redux-worker/main';
import Foo from '../components/Foo';
const mapDispatchToProps = (dispatch, selectedOwnProps) => ({
// ...
});
const ownPropSelector = componentProps => ({
// ...
});
// The mapStateToProps selector is placed at `Foo.selector.js` file
export default connectWorker('FOO_BRIDGE', mapDispatchToProps, ownPropsSelector)(Foo);
// --------------------
// Foo.selector.js (worker context)
// --------------------
import { registerSelector } from '@ackee/redux-worker/worker';
const mapStateToProps = (state, selectedOwnProps) => ({
// ...
});
registerSelector('FOO_BRIDGE', mapStateToProps);
API - Worker context
registerSelector(bridgeId: String, mapStateToProps: Function): void
Add a container selector to global selectors register.
import { registerSelector } from '@ackee/redux-worker';
const mapStateToProps = state => ({
// ...
});
registerSelector('BRIDGE_ID', mapStateToProps);
async executeInWindow(pathToProperty: String, args:Array?): any
import { executeInWindow } from '@ackee/redux-worker/worker';
async function accessWindowObjectInWorker() {
const innerWidth = await executeInWindow('innerWidth');
console.log(innerWidth);
}
async function goBack() {
await executeInWindow('history.back');
}
async function storeToSessionStorage() {
await executeInWindow('sessionStorage.setItem', ['message', 'hello world']);
const message = await executeInWindow('sessionStorage.getItem', ['message']);
console.log(message); // > 'hello world'
}
API - shared context
unqiueId(prefix?): String
Get unique string ID, optionally with custom prefix. The uniqueness is guaranteed under the same context.
The purpose of this utility is to generate bridge IDs for
connectWorker
HOC andregisterSelector
method.
Example
import { uniqueId } from '@ackee/redux-worker';
uniqueId(); // > 'rs'
uniqueId('COUNTER_BRIDGE'); // > 'COUNTER_BRIDGE_rt'
uniqueId('COUNTER_BRIDGE'); // > 'COUNTER_BRIDGE_ru'