Navigate back to the homepage
BLOG
Try NowLogin
Back

Redux In Web Workers: Off-Main-Thread Redux Reducers and Middleware

Daishi Kato
October 2nd, 2020 · 2 min read

Introduction

Redux is a framework-agnostic library for global state. It’s often used with React.

While I like the abstraction of Redux, React will introduce Concurrent Mode in the near future. If we want to get benefit of useTransition, state must be inside React to allow state branching. That means we can’t get the benefit with Redux.

I’ve been developing React Tracked for global state that allows state branching. It works well in Concurrent Mode. That leaves me a question: What is a use case that only Redux can do.

The reason Redux can’t allow state branching is that the state is in the external store. So, what is the benefit of having an external store. Redux Toolkit can be one answer. I have another answer, an external store allow off main thread.

React is a UI library, and it’s intended to run in the main UI thread. Redux is usually UI agnostic, so we can run it in a worker thread.

There has been several experiments to off load Redux from the main thread, and run some or all of Redux work in Web Workers. I’ve developed a library for off load the entire Redux store.

redux-in-worker

The library is called redux-in-worker. Please check out the GitHub repository.

https://github.com/dai-shi/redux-in-worker

Although this library is not dependent on React, it’s developed with the mind to be used with React. That is, it will make sure to keep object referential equality, which allows to prevent unnecessary re-renders in React.

Please check out the blog post I wrote about it.

Off-main-thread React Redux with Performance

In the next sections, I will show some code to work with async actions with redux-in-worker.

redux-api-middleware

redux-api-middleware is one of the libraries that existed from the early days. It receives actions and run API calls described in the actions. The action object is serializable, so we can send it to the worker without any problems.

Here’s the example code:

1import { createStore, applyMiddleware } from 'redux';
2import { apiMiddleware } from 'redux-api-middleware';
3
4import { exposeStore } from 'redux-in-worker';
5
6export const initialState = {
7 count: 0,
8 person: {
9 name: '',
10 loading: false,
11 },
12};
13
14export type State = typeof initialState;
15
16export type Action =
17 | { type: 'increment' }
18 | { type: 'decrement' }
19 | { type: 'setName'; name: string }
20 | { type: 'REQUEST' }
21 | { type: 'SUCCESS'; payload: { name: string } }
22 | { type: 'FAILURE' };
23
24const reducer = (state = initialState, action: Action) => {
25 console.log({ state, action });
26 switch (action.type) {
27 case 'increment': return {
28 ...state,
29 count: state.count + 1,
30 };
31 case 'decrement': return {
32 ...state,
33 count: state.count - 1,
34 };
35 case 'setName': return {
36 ...state,
37 person: {
38 ...state.person,
39 name: action.name,
40 },
41 };
42 case 'REQUEST': return {
43 ...state,
44 person: {
45 ...state.person,
46 loading: true,
47 },
48 };
49 case 'SUCCESS': return {
50 ...state,
51 person: {
52 ...state.person,
53 name: action.payload.name,
54 loading: false,
55 },
56 };
57 case 'FAILURE': return {
58 ...state,
59 person: {
60 ...state.person,
61 name: 'ERROR',
62 loading: false,
63 },
64 };
65 default: return state;
66 }
67};
68
69const store = createStore(reducer, applyMiddleware(apiMiddleware));
70
71exposeStore(store);

The above code run in a worker.

The code run in the main thread is the following:

1import { wrapStore } from 'redux-in-worker';
2import { initialState } from './store.worker';
3
4const store = wrapStore(
5 new Worker('./store.worker', { type: 'module' }),
6 initialState,
7 window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__(),
8);

Please find the full example in the repository:

https://github.com/dai-shi/redux-in-worker/tree/master/examples/04_api

redux-saga

Another library that can be used with redux-in-worker is redux-saga. It’s a powerful library for any async functions with generators. Because its action object is serializable, it just works.

Here’s the example code:

1import { createStore, applyMiddleware } from 'redux';
2import createSagaMiddleware from 'redux-saga';
3import {
4 call,
5 put,
6 delay,
7 takeLatest,
8 takeEvery,
9 all,
10} from 'redux-saga/effects';
11
12import { exposeStore } from 'redux-in-worker';
13
14const sagaMiddleware = createSagaMiddleware();
15
16export const initialState = {
17 count: 0,
18 person: {
19 name: '',
20 loading: false,
21 },
22};
23
24export type State = typeof initialState;
25
26type ReducerAction =
27 | { type: 'INCREMENT' }
28 | { type: 'DECREMENT' }
29 | { type: 'SET_NAME'; name: string }
30 | { type: 'START_FETCH_USER' }
31 | { type: 'SUCCESS_FETCH_USER'; name: string }
32 | { type: 'ERROR_FETCH_USER' };
33
34type AsyncActionFetch = { type: 'FETCH_USER'; id: number }
35type AsyncActionDecrement = { type: 'DELAYED_DECREMENT' };
36type AsyncAction = AsyncActionFetch | AsyncActionDecrement;
37
38export type Action = ReducerAction | AsyncAction;
39
40function* userFetcher(action: AsyncActionFetch) {
41 try {
42 yield put<ReducerAction>({ type: 'START_FETCH_USER' });
43 const response = yield call(() => fetch(`https://jsonplaceholder.typicode.com/users/${action.id}`));
44 const data = yield call(() => response.json());
45 yield delay(500);
46 const { name } = data;
47 if (typeof name !== 'string') throw new Error();
48 yield put<ReducerAction>({ type: 'SUCCESS_FETCH_USER', name });
49 } catch (e) {
50 yield put<ReducerAction>({ type: 'ERROR_FETCH_USER' });
51 }
52}
53
54function* delayedDecrementer() {
55 yield delay(500);
56 yield put<ReducerAction>({ type: 'DECREMENT' });
57}
58
59function* userFetchingSaga() {
60 yield takeLatest<AsyncActionFetch>('FETCH_USER', userFetcher);
61}
62
63function* delayedDecrementingSaga() {
64 yield takeEvery<AsyncActionDecrement>('DELAYED_DECREMENT', delayedDecrementer);
65}
66
67function* rootSaga() {
68 yield all([
69 userFetchingSaga(),
70 delayedDecrementingSaga(),
71 ]);
72}
73
74const reducer = (state = initialState, action: ReducerAction) => {
75 console.log({ state, action });
76 switch (action.type) {
77 case 'INCREMENT': return {
78 ...state,
79 count: state.count + 1,
80 };
81 case 'DECREMENT': return {
82 ...state,
83 count: state.count - 1,
84 };
85 case 'SET_NAME': return {
86 ...state,
87 person: {
88 ...state.person,
89 name: action.name,
90 },
91 };
92 case 'START_FETCH_USER': return {
93 ...state,
94 person: {
95 ...state.person,
96 loading: true,
97 },
98 };
99 case 'SUCCESS_FETCH_USER': return {
100 ...state,
101 person: {
102 ...state.person,
103 name: action.name,
104 loading: false,
105 },
106 };
107 case 'ERROR_FETCH_USER': return {
108 ...state,
109 person: {
110 ...state.person,
111 name: 'ERROR',
112 loading: false,
113 },
114 };
115 default: return state;
116 }
117};
118
119const store = createStore(reducer, applyMiddleware(sagaMiddleware));
120sagaMiddleware.run(rootSaga);
121
122exposeStore(store);

The above code run in a worker.

The code run in the main thread is the following:

1import { wrapStore } from 'redux-in-worker';
2import { initialState } from './store.worker';
3
4const store = wrapStore(
5 new Worker('./store.worker', { type: 'module' }),
6 initialState,
7 window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__(),
8);

This is exactly the same as the previous example.

Please find the full example in the repository:

https://github.com/dai-shi/redux-in-worker/tree/master/examples/05_saga

Closing notes (Redux Thunk)

One of the biggest hurdles in this approach is redux-thunk. redux-thunk takes a function action which is not serializable. It’s the official tool and included in Redux Toolkit too. This implies this approach is not going to be mainstream.

But anyway, I wish somebody likes this approach and evaluates in some real environments. Please feel free to open a discussion in GitHub issues.

By the way, I have developed another library for React to use Web Workers:

https://github.com/dai-shi/react-hooks-worker

This library lets you off-main-thread any functions. It’s a small library and fairly stable. Check it out too.

Read the original article or more interesting posts on Daishi’s blog.

Frontend Monitoring

Asayer is a frontend monitoring tool that replays everything your users do and shows how your web app behaves for every issue. It lets you reproduce issues, aggregate JS errors and monitor your web app’s performance.

Happy debugging, for modern frontend teams - Start monitoring your web app for free.

More articles from Asayer Blog

How to Measure Performance in JavaScript Applications

How to accurately measure the performance of your JavaScript application using the browser's Performance APIs (now, mark and measure).

September 25th, 2020 · 6 min read

How to Debug Javascript Apps with Chrome DevTools

The tools and tactics developers use to debug their javascript applications (Vue, Angular, React) with Chrome DevTools.

September 18th, 2020 · 7 min read
© 2020 Asayer Blog
Link to $https://twitter.com/asayerioLink to $https://github.com/asayerioLink to $https://www.linkedin.com/company/18257552