Search code examples
javascriptreactjsreduxreact-reduxredux-toolkit

How can I make batching work with async dispatch calls?


Since React 18, state updates are automatically batched. When using Redux, batches are also used. However, it doesn't seem to work when using async dispatch calls. For example, for the thunk:

const incrementAsync = createAsyncThunk("incrementAsync", (_, { dispatch }) => {
  setTimeout(() => {
    dispatch(increment());
  }, 1000);
});

The following will make the component rerender three times (but visually the counter only changes once):

const increment = () => {
  dispatch(incrementAsync());
  dispatch(incrementAsync());
  dispatch(incrementAsync());
};

CodeSandbox demo

I don't know if it is the expected behavior, but how can I do so these updates are batched? I would like my component to rerender only once, regardless of the number of updates.


Please note:
I only made these snippets for the question, my code has nothing to do with simple counters and I need the calls to be async.


Solution

  • @reduxjs/toolkit: ^1.9.5

    You can use autoBatchEnhancer

    A Redux store enhancer that looks for one or more "low-priority" dispatched actions in a row, and queues a callback to run subscriber notifications on a delay. It then notifies subscribers either when the queued callback runs, or when the next "normal-priority" action is dispatched, whichever is first.

    counterSlice.js:

    import { createSlice, createAsyncThunk, prepareAutoBatched, SHOULD_AUTOBATCH } from '@reduxjs/toolkit';
    
    export const incrementAsync = createAsyncThunk('incrementAsync', (_, { dispatch }) => {
        setTimeout(() => {
            dispatch(increment());
        }, 1000);
    });
    
    export const counterSlice = createSlice({
        name: 'counter',
        initialState: {
            value: 0,
        },
        reducers: {
            increment: {
                reducer: (state, action) => {
                    console.log('action: ', action);
                    state.value += 1;
                },
                prepare: prepareAutoBatched(),
            },
        }
    });
    
    export const { increment } = counterSlice.actions;
    
    export const selectCount = (state) => state.counter.value;
    
    export default counterSlice.reducer;
    

    store.js:

    import { configureStore, autoBatchEnhancer } from '@reduxjs/toolkit';
    import counterReducer from '../features/counter/counterSlice';
    
    export default configureStore({
        reducer: {
            counter: counterReducer,
        },
        enhancers: (existingEnhancers) => {
            // Add the autobatch enhancer to the store setup
            return existingEnhancers.concat(autoBatchEnhancer());
        },
    });
    

    codesandbox