diff --git a/src/devTools.js b/src/devTools.js index 0ba40b30..6ff13f65 100644 --- a/src/devTools.js +++ b/src/devTools.js @@ -91,6 +91,7 @@ function liftReducer(reducer, monitorReducer, initialState) { * Manages how the DevTools actions modify the DevTools state. */ return function liftedReducer(liftedState = initialLiftedState, liftedAction) { + let shouldRecomputeStates = true; let { committedState, stagedActions, @@ -127,6 +128,8 @@ function liftReducer(reducer, monitorReducer, initialState) { break; case ActionTypes.JUMP_TO_STATE: currentStateIndex = liftedAction.index; + // Optimization: we know the history has not changed. + shouldRecomputeStates = false; break; case ActionTypes.SWEEP: stagedActions = stagedActions.filter((_, i) => !skippedActions[i]); @@ -138,19 +141,34 @@ function liftReducer(reducer, monitorReducer, initialState) { if (currentStateIndex === stagedActions.length - 1) { currentStateIndex++; } + stagedActions = [...stagedActions, liftedAction.action]; timestamps = [...timestamps, liftedAction.timestamp]; + + // Optimization: we know that the past has not changed. + shouldRecomputeStates = false; + // Instead of recomputing the states, append the next one. + const previousEntry = computedStates[computedStates.length - 1]; + const nextEntry = computeNextEntry( + reducer, + liftedAction.action, + previousEntry.state, + previousEntry.error + ); + computedStates = [...computedStates, nextEntry]; break; default: break; } - computedStates = recomputeStates( - reducer, - committedState, - stagedActions, - skippedActions - ); + if (shouldRecomputeStates) { + computedStates = recomputeStates( + reducer, + committedState, + stagedActions, + skippedActions + ); + } monitorState = monitorReducer(monitorState, liftedAction); diff --git a/test/devTools.spec.js b/test/devTools.spec.js index 10bf33b9..d7540741 100644 --- a/test/devTools.spec.js +++ b/test/devTools.spec.js @@ -184,4 +184,35 @@ describe('devTools', () => { storeWithBug.dispatch({ type: 'SET_UNDEFINED' }); expect(storeWithBug.getState()).toBe(2); }); + + it('should not recompute states on every action', () => { + let reducerCalls = 0; + let monitoredStore = devTools()(createStore)(() => reducerCalls++); + expect(reducerCalls).toBe(1); + monitoredStore.dispatch({ type: 'INCREMENT' }); + monitoredStore.dispatch({ type: 'INCREMENT' }); + monitoredStore.dispatch({ type: 'INCREMENT' }); + expect(reducerCalls).toBe(4); + }); + + it('should not recompute states when jumping to state', () => { + let reducerCalls = 0; + let monitoredStore = devTools()(createStore)(() => reducerCalls++); + let monitoredDevToolsStore = monitoredStore.devToolsStore; + + expect(reducerCalls).toBe(1); + monitoredStore.dispatch({ type: 'INCREMENT' }); + monitoredStore.dispatch({ type: 'INCREMENT' }); + monitoredStore.dispatch({ type: 'INCREMENT' }); + expect(reducerCalls).toBe(4); + + monitoredDevToolsStore.dispatch(ActionCreators.jumpToState(0)); + expect(reducerCalls).toBe(4); + + monitoredDevToolsStore.dispatch(ActionCreators.jumpToState(1)); + expect(reducerCalls).toBe(4); + + monitoredDevToolsStore.dispatch(ActionCreators.jumpToState(3)); + expect(reducerCalls).toBe(4); + }); });