A set of utilities for running Redux in a web worker.
Add the registry to .npmrc:
@alorel:registry=https://npm.pkg.github.comThen install the library and its fast-json-patch dependency:
npm install fast-json-patch @alorel/redux-off-main-threadTypescript users should additionally install the following packages for the typings:
npm install redux redux-devtools-extensionThe main thread should be used by the UI, not state management. This set of utilities aids in running Redux in a web worker so your main thread doesn't slow down the UI! The general process is as follows:
- Initialise your store with all its reducers on a web worker, apply the off main thread middleware.
- Initialise a Worker wrapper on the main thread - this has the same API as a regular store, but with a few notable differences:
- It does not have any reducers,
replaceReducerthrows an error - It does not have
[Symbol.observable] - Actions do not synchronously update the state anymore, therefore the
subscribe()function may not behave as expected
- It does not have any reducers,
- Use the store as usual: dispatch an action.
- The action is serialised and sent to the web worker.
- The worker passes it on to the real redux store - reducers are triggered at this point.
- A diff of the state change is produced - this is where the
fast-json-patchdependency comes in - and sent to the main thread along with the action that triggered it.- It would be much simpler to just overwrite the entire state object, but that would kill all the old object references and could potentially have a terrible effect on app performance as well as introducing bugs
- The main thread's store wrapper clones only the paths that changed and applies the diff to the new state object.
- A change is emitted.
// common.js
export const STORE_DEFAULT_STATE = {
foo: 'bar'
};// index.js
import {createWrappedStore} from '@alorel/redux-off-main-thread/main-thread';
import {STORE_DEFAULT_STATE} from './common';
const worker = new Worker('/worker.js');
const store = createWrappedStore({
// Your store's initial state
initialState: STORE_DEFAULT_STATE,
worker
});
store.dispatch({type: 'some-action'});// worker.js
import {onReduxWorkerThreadReady, createReduxOMTMiddleware} from '@alorel/redux-off-main-thread/worker';
import {applyMiddleware, createStore} from 'redux';
import {STORE_DEFAULT_STATE} from './common';
/*
* Optional, but lets you know when the main thread's finished adding event listeners - should be instant unless
* you've created some weird setup for testing and the like
*/
onReduxWorkerThreadReady()
.then(() => {
// The redux-off-main-thread middleware should always be last
const store = createStore(someReducerFunction, STORE_DEFAULT_STATE, applyMiddleware(createReduxOMTMiddleware()));
// use the store as you please - it's now hooked up.
});Simply set syncInitialState to true when creating the wrapped store.
// index.js
import {createWrappedStore} from '@alorel/redux-off-main-thread/main-thread';
const worker = new Worker('/worker.js');
const store = createWrappedStore({
initialState: {some: {default: 'state'}},
syncInitialState: true,
worker
});// worker.js
import {onReduxWorkerThreadInitialStateReceived, createReduxOMTMiddleware} from '@alorel/redux-off-main-thread/worker';
import {applyMiddleware, createStore} from 'redux';
onReduxWorkerThreadInitialStateReceived()
.then(initialState => {
// The redux-off-main-thread middleware should always be last
const store = createStore(someReducerFunction, initialState, applyMiddleware(createReduxOMTMiddleware()));
// use the store as you please - it's now hooked up.
});Devtools are inaccessible by the worker thread so they can't be used normally. Additionally, actions don't synchronously
update the state anymore, making devtools somewhat useless. Simply pass a devtoolsInit option with either the enhancer
config object or true, which will default to {}.
// index.js
import {createWrappedStore} from '@alorel/redux-off-main-thread/main-thread';
const worker = new Worker('/worker.js');
const store = createWrappedStore({
devtoolsInit: true, // or pass an options object - see API
initialState: {some: {default: 'state'}},
worker
});// index.js
import {resolveWrappedStore} from '@alorel/redux-off-main-thread/main-thread';
const worker = new Worker('/worker.js');
// Same options as createWrappedStore, but initialState & syncInitialState are not allowed
resolveWrappedStore({worker})
.then(store => {
store.dispatch({type: 'foo'});
})// worker.js
import {provideReduxOMTInitialState} from '@alorel/redux-off-main-thread/worker';
provideReduxOMTInitialState({someInitialState: 'foo'})
.then(() => {
// main thread promise resolved
});Typescript definitions are provided for clarity
import {Middleware} from 'redux';
/** Create a redux-off-main-thread middleware instance. This should be run on the worker thread. */
export declare function createReduxOMTMiddleware(): Middleware;
/**
* Resolves with the initial state when the worker receives an initial state message.
* Rejects when called outside a worker thread.
*/
export declare function onReduxWorkerThreadInitialStateReceived(): Promise<any>;
/**
* Resolves when the worker receives a ready event, indicating that the main thread has finished setting up
* event listeners. Should be instant unless you've created some weird environment e.g. during CI.
* Rejects when called outside a worker thread.
*/
export declare function onReduxWorkerThreadReady(): Promise<void>;
/**
* Used to provide the initial state to a main thread worker initialised via {@link resolveWrappedStore}. This function
* should be called immediately on the worker entrypoint.
* @return A void promise that resolves once the initial state request has been fulfilled.
*/
export declare function provideReduxOMTInitialState<S = any>(state: S): Promise<void>;import type {Action, AnyAction, Store} from 'redux';
import type {EnhancerOptions} from 'redux-devtools-extension';
export declare type WorkerPartial = Pick<Worker, 'addEventListener' | 'postMessage' | 'removeEventListener'>;
/** A Redux store wrapped to run off the main thread */
export type WrappedStore<S, A extends Action = AnyAction> = Store<S, A> & {
/**
* Actions no longer mutate the state synchronously, therefore the store no longer behaves exactly as a regular
* Redux store:
* <code>
* const oldState = store.getState();
* store.dispatch({type: 'some-valid-action-that-should-mutate-the-state''});
* // True on an off-main-thread store, false on a regular store
* console.log(oldState === store.getState());
* </code>
* This method can be used to react to when the store off the main thread
*/
onChange(listener: (action: A, newState: S, oldState: S) => void): () => void;
}
/** {@link createWrappedStore} initialisation config */
export interface CreateWrappedStoreInit<S> {
/**
* Options for enabling devtools support. Can be either an {@link EnhancerOptions} object or true,
* which is equivalent to passing {}
* @default false
*/
devtoolsInit?: boolean | EnhancerOptions;
/** Initial store state */
initialState: S;
/**
* Having this as false requires the main thread and worker thread to set the same initial state from an object
* somewhere in your codebase (and bundled by your build system) and is suitable for the
* {@link https://github.com/Alorel/redux-off-main-thread/tree/master#basic-usage Basic usage} use case. You may
* instead opt to only set this to true and send the initial state as a message to the worker; this is outlined in the
* {@link https://github.com/Alorel/redux-off-main-thread/tree/master#sending-default-state-from-the-main-thread Sending default state from the main thread}
* example.
* @default false
*/
syncInitialState?: boolean;
/** The worker instance Redux is running on */
worker: WorkerPartial;
}
/**
* Create a wrapped store with the same API as a regular Redux store bar several differences:
* <ul>
* <li>It does not have any reducers, replaceReducer throws an error</li>
* <li>It does not have a Symbol.observable</li>
* <li>Actions do not synchronously update the state anymore, therefore the subscribe() function may not behave as expected</li>
* <li>It has an extra onChange() method</li>
* </ul>
* @param init
*/
export declare function createWrappedStore<S, A extends Action = AnyAction>(init: CreateWrappedStoreInit<S>): WrappedStore<S, A>;
/** Same as a regular {@link CreateWrappedStoreInit}, but with initialState & syncInitialState omitted */
export declare type ResolveWrappedStoreInit<S> = Omit<CreateWrappedStoreInit<S>, 'initialState' | 'syncInitialState'>;
/**
* Similar to {@link createWrappedStore}, but the store on the worker is used to provide the initial state via
* {@link provideReduxOMTInitialState}.
* @return A promise that resolves with a {@link WrappedStore} when the worker store is initialised.
*/
export declare function resolveWrappedStore<S, A extends Action>(init: ResolveWrappedStoreInit<S>): Promise<WrappedStore<S, A>>;