This commit is contained in:
Tomáš Weiss 2016-04-02 17:18:41 +00:00
commit 0eced6814b
3 changed files with 103 additions and 6 deletions

View File

@ -370,6 +370,8 @@ Note that there are no useful props you can pass to the `DevTools` component oth
* **It is important that `DevTools.instrument()` store enhancer should be added to your middleware stack *after* `applyMiddleware` in the `compose`d functions, as `applyMiddleware` is potentially asynchronous.** Otherwise, DevTools wont see the raw actions emitted by asynchronous middleware such as [redux-promise](https://github.com/acdlite/redux-promise) or [redux-thunk](https://github.com/gaearon/redux-thunk). * **It is important that `DevTools.instrument()` store enhancer should be added to your middleware stack *after* `applyMiddleware` in the `compose`d functions, as `applyMiddleware` is potentially asynchronous.** Otherwise, DevTools wont see the raw actions emitted by asynchronous middleware such as [redux-promise](https://github.com/acdlite/redux-promise) or [redux-thunk](https://github.com/gaearon/redux-thunk).
* **Are you a library author**? If you're building a Store Enhancer you might find handy to know which actions have been dispatched regularly and which have been replayed. `redux-devtools` is calling reducer with third argument. The argument is a boolean identifying that Action has been replayed.
### What Next? ### What Next?
Now that you see the DevTools, you might want to learn what those buttons mean and how to use them. This usually depends on the monitor. You can begin by exploring the [LogMonitor](https://github.com/gaearon/redux-devtools-log-monitor) and [DockMonitor](https://github.com/gaearon/redux-devtools-dock-monitor) documentation, as they are the default monitors we suggest to use together. When youre comfortable using them, you may want to create your own monitor for more exotic purposes, such as a [chart](https://github.com/romseguy/redux-devtools-chart-monitor) or a [diff](https://github.com/whetstone/redux-devtools-diff-monitor) monitor. Dont forget to send a PR to feature your monitor at the front page! Now that you see the DevTools, you might want to learn what those buttons mean and how to use them. This usually depends on the monitor. You can begin by exploring the [LogMonitor](https://github.com/gaearon/redux-devtools-log-monitor) and [DockMonitor](https://github.com/gaearon/redux-devtools-dock-monitor) documentation, as they are the default monitors we suggest to use together. When youre comfortable using them, you may want to create your own monitor for more exotic purposes, such as a [chart](https://github.com/romseguy/redux-devtools-chart-monitor) or a [diff](https://github.com/whetstone/redux-devtools-diff-monitor) monitor. Dont forget to send a PR to feature your monitor at the front page!

View File

@ -65,7 +65,7 @@ const INIT_ACTION = { type: '@@INIT' };
/** /**
* Computes the next entry in the log by applying an action. * Computes the next entry in the log by applying an action.
*/ */
function computeNextEntry(reducer, action, state, error) { function computeNextEntry(reducer, action, state, error, replaying) {
if (error) { if (error) {
return { return {
state, state,
@ -76,7 +76,7 @@ function computeNextEntry(reducer, action, state, error) {
let nextState = state; let nextState = state;
let nextError; let nextError;
try { try {
nextState = reducer(state, action); nextState = reducer(state, { ...action, replaying });
} catch (err) { } catch (err) {
nextError = err.toString(); nextError = err.toString();
if (typeof window === 'object' && typeof window.chrome !== 'undefined') { if (typeof window === 'object' && typeof window.chrome !== 'undefined') {
@ -103,7 +103,8 @@ function recomputeStates(
committedState, committedState,
actionsById, actionsById,
stagedActionIds, stagedActionIds,
skippedActionIds skippedActionIds,
replaying
) { ) {
// Optimization: exit early and return the same reference // Optimization: exit early and return the same reference
// if we know nothing could have changed. // if we know nothing could have changed.
@ -126,7 +127,7 @@ function recomputeStates(
const shouldSkip = skippedActionIds.indexOf(actionId) > -1; const shouldSkip = skippedActionIds.indexOf(actionId) > -1;
const entry = shouldSkip ? const entry = shouldSkip ?
previousEntry : previousEntry :
computeNextEntry(reducer, action, previousState, previousError); computeNextEntry(reducer, action, previousState, previousError, replaying);
nextComputedStates.push(entry); nextComputedStates.push(entry);
} }
@ -201,6 +202,10 @@ function liftReducerWith(reducer, initialCommittedState, monitorReducer, options
// value whenever we feel like we don't have to recompute the states. // value whenever we feel like we don't have to recompute the states.
let minInvalidatedStateIndex = 0; let minInvalidatedStateIndex = 0;
// For now, potentially any action except PERFORM_ACTION is considered
// as replay
let replaying = true;
switch (liftedAction.type) { switch (liftedAction.type) {
case ActionTypes.RESET: { case ActionTypes.RESET: {
// Get back to the state the store was created with. // Get back to the state the store was created with.
@ -297,6 +302,10 @@ function liftReducerWith(reducer, initialCommittedState, monitorReducer, options
stagedActionIds = [...stagedActionIds, actionId]; stagedActionIds = [...stagedActionIds, actionId];
// Optimization: we know that only the new action needs computing. // Optimization: we know that only the new action needs computing.
minInvalidatedStateIndex = stagedActionIds.length - 1; minInvalidatedStateIndex = stagedActionIds.length - 1;
// This is the first time Action is actually performed, therefore
// we don't consider this replay
replaying = false;
break; break;
} }
case ActionTypes.IMPORT_STATE: { case ActionTypes.IMPORT_STATE: {
@ -314,6 +323,8 @@ function liftReducerWith(reducer, initialCommittedState, monitorReducer, options
break; break;
} }
case '@@redux/INIT': { case '@@redux/INIT': {
replaying = false;
// Always recompute states on hot reload and init. // Always recompute states on hot reload and init.
minInvalidatedStateIndex = 0; minInvalidatedStateIndex = 0;
@ -352,7 +363,8 @@ function liftReducerWith(reducer, initialCommittedState, monitorReducer, options
committedState, committedState,
actionsById, actionsById,
stagedActionIds, stagedActionIds,
skippedActionIds skippedActionIds,
replaying
); );
monitorState = monitorReducer(monitorState, liftedAction); monitorState = monitorReducer(monitorState, liftedAction);
return { return {

View File

@ -1,4 +1,4 @@
import expect, { spyOn } from 'expect'; import expect, { createSpy, spyOn } from 'expect';
import { createStore, compose } from 'redux'; import { createStore, compose } from 'redux';
import instrument, { ActionCreators } from '../src/instrument'; import instrument, { ActionCreators } from '../src/instrument';
@ -542,4 +542,87 @@ describe('instrument', () => {
'Check your store configuration.' 'Check your store configuration.'
); );
}); });
describe('replaying flag', () => {
const TESTING_ACTION = { type: 'TESTING_ACTION' };
const INIT_ACTION = { type: '@@INIT' };
const TESTING_APP_STATE = 42;
const buildTestingAction = replaying => ({ ...TESTING_ACTION, replaying });
const buildInitAction = replaying => ({ ...INIT_ACTION, replaying });
let spiedEmptyReducer;
let replayingStore;
let liftedReplayingStore;
beforeEach(() => {
spiedEmptyReducer = createSpy(function emptyReducer(appState = TESTING_APP_STATE) {
return appState;
}).andCallThrough();
replayingStore = createStore(spiedEmptyReducer, instrument());
liftedReplayingStore = replayingStore.liftedStore;
});
it('should provide falsy replaying flag when plain action is dispatched', () => {
replayingStore.dispatch(TESTING_ACTION);
expect(spiedEmptyReducer).toHaveBeenCalled();
expect(spiedEmptyReducer.calls[1].arguments).toEqual([TESTING_APP_STATE, buildTestingAction(false)]);
});
it('should provide falsy replaying flag when PERFORM_ACTION is dispatched', () => {
replayingStore.dispatch(TESTING_ACTION);
liftedReplayingStore.dispatch(ActionCreators.performAction(TESTING_ACTION));
expect(spiedEmptyReducer.calls[1].arguments).toEqual([TESTING_APP_STATE, buildTestingAction(false)]);
});
it('should provide truthy replaying flag for init action which follows rollback', () => {
replayingStore.dispatch(TESTING_ACTION);
liftedReplayingStore.dispatch(ActionCreators.rollback());
expect(spiedEmptyReducer.calls[2].arguments).toEqual([undefined, buildInitAction(true)]);
});
it('should provide truthy replaying flag for init action which follows reset', () => {
replayingStore.dispatch(TESTING_ACTION);
liftedReplayingStore.dispatch(ActionCreators.reset());
expect(spiedEmptyReducer.calls[2].arguments).toEqual([undefined, buildInitAction(true)]);
});
it('should provide truthy replaying flag for init action which follows commit', () => {
replayingStore.dispatch(TESTING_ACTION);
liftedReplayingStore.dispatch(ActionCreators.commit());
expect(spiedEmptyReducer.calls[2].arguments).toEqual([42, buildInitAction(true)]);
});
it('should provide truthy replaying flag for all the actions after sweeping', () => {
replayingStore.dispatch(TESTING_ACTION);
liftedReplayingStore.dispatch(ActionCreators.sweep());
expect(spiedEmptyReducer.calls[2].arguments).toEqual([undefined, buildInitAction(true)]);
expect(spiedEmptyReducer.calls[3].arguments).toEqual([TESTING_APP_STATE, buildTestingAction(true)]);
});
it('after toggling, should provide truthy replaying flag for action which has not been toggled', () => {
const NEXT_TESTING_ACTION = { type: 'NEXT_TESTING_ACTION' };
replayingStore.dispatch(TESTING_ACTION);
replayingStore.dispatch(NEXT_TESTING_ACTION);
liftedReplayingStore.dispatch(ActionCreators.toggleAction(1));
expect(spiedEmptyReducer.calls[3].arguments).toEqual([TESTING_APP_STATE, { ...NEXT_TESTING_ACTION, replaying: true }]);
});
it('should provide truthy replaying flag for all the actions after importing state', () => {
replayingStore.dispatch(TESTING_ACTION);
const exportedState = liftedReplayingStore.getState();
const spiedImportStoreReducer = createSpy(function importReducer(appState = TESTING_APP_STATE) {
return appState;
}).andCallThrough();
const importStore = createStore(spiedImportStoreReducer, instrument());
importStore.liftedStore.dispatch(ActionCreators.importState(exportedState));
expect(spiedImportStoreReducer.calls[0].arguments).toEqual([undefined, buildInitAction(false)]);
expect(spiedImportStoreReducer.calls[1].arguments).toEqual([undefined, buildInitAction(true)]);
expect(spiedImportStoreReducer.calls[2].arguments).toEqual([TESTING_APP_STATE, buildTestingAction(true)]);
});
});
}); });