Allow monitors to have their own reducers

This commit is contained in:
Dan Abramov 2015-09-27 16:10:46 +03:00
parent a37b30e878
commit 4ac31f985a
7 changed files with 104 additions and 72 deletions

View File

@ -26,10 +26,10 @@
"babel-core": "^5.6.18", "babel-core": "^5.6.18",
"babel-loader": "^5.1.4", "babel-loader": "^5.1.4",
"node-libs-browser": "^0.5.2", "node-libs-browser": "^0.5.2",
"react-dock": "0.0.3", "react-dock": "^0.1.0",
"react-hot-loader": "^1.3.0", "react-hot-loader": "^1.3.0",
"redux-devtools": "^3.0.0-alpha-1", "redux-devtools": "^3.0.0-alpha-1",
"redux-devtools-log-monitor": "^1.0.0-alpha", "redux-devtools-log-monitor": "^1.0.0-alpha-3",
"webpack": "^1.9.11", "webpack": "^1.9.11",
"webpack-dev-server": "^1.9.0" "webpack-dev-server": "^1.9.0"
} }

View File

@ -2,10 +2,13 @@ import { createStore, applyMiddleware, compose } from 'redux';
import { devTools, persistState } from 'redux-devtools'; import { devTools, persistState } from 'redux-devtools';
import thunk from 'redux-thunk'; import thunk from 'redux-thunk';
import rootReducer from '../reducers'; import rootReducer from '../reducers';
import { createMonitorReducer } from 'redux-devtools-log-monitor';
const finalCreateStore = compose( const finalCreateStore = compose(
applyMiddleware(thunk), applyMiddleware(thunk),
devTools(), devTools(createMonitorReducer({
isVisibleOnLoad: true
})),
persistState(window.location.href.match(/[?&]debug_session=([^&]+)\b/)) persistState(window.location.href.match(/[?&]debug_session=([^&]+)\b/))
)(createStore); )(createStore);

View File

@ -50,10 +50,10 @@
"webpack": "^1.11.0" "webpack": "^1.11.0"
}, },
"peerDependencies": { "peerDependencies": {
"redux": "^2.0.0 || ^3.0.0" "redux": "^3.0.0"
}, },
"dependencies": { "dependencies": {
"react-redux": "^3.0.0", "react-redux": "^3.0.0",
"redux": "^2.0.0 || ^3.0.0" "redux": "^3.0.0"
} }
} }

View File

@ -1,42 +1,54 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { ActionCreators } from './devTools'; import { ActionCreators } from './devTools';
export default function connectMonitor(Monitor) { export default function connectMonitor(monitorActionCreators = {}) {
const ConnectedMonitor = connect(state => state, ActionCreators)(Monitor); return Monitor => {
function mapStateToProps(state) {
class DevTools extends Component { return state;
static Monitor = Monitor;
static propTypes = {
store(props, propName, componentName) {
if (!props.store) {
return new Error('Required prop `store` was not specified in `' + componentName + '`.');
}
if (!props.store.devToolsStore) {
return new Error(
'Could not find the DevTools store inside the `store` prop passed to `' +
componentName +
'`. Have you applied the devTools() store enhancer?'
);
}
}
};
render() {
const { store } = this.props;
if (!store) {
return null;
}
const { devToolsStore } = store;
if (!devToolsStore) {
return null;
}
return <ConnectedMonitor {...this.props} store={devToolsStore} />;
} }
} function mapDispatchToProps(dispatch) {
return {
...bindActionCreators(ActionCreators, dispatch),
monitorActions: bindActionCreators(monitorActionCreators, dispatch)
};
}
const ConnectedMonitor = connect(mapStateToProps, mapDispatchToProps)(Monitor);
return DevTools; class DevTools extends Component {
static Monitor = Monitor;
static propTypes = {
store(props, propName, componentName) {
if (!props.store) {
return new Error('Required prop `store` was not specified in `' + componentName + '`.');
}
if (!props.store.devToolsStore) {
return new Error(
'Could not find the DevTools store inside the `store` prop passed to `' +
componentName +
'`. Have you applied the devTools() store enhancer?'
);
}
}
};
render() {
const { store } = this.props;
if (!store) {
return null;
}
const { devToolsStore } = store;
if (!devToolsStore) {
return null;
}
return <ConnectedMonitor {...this.props} store={devToolsStore} />;
}
}
return DevTools;
};
} }

View File

@ -6,7 +6,6 @@ const ActionTypes = {
SWEEP: 'SWEEP', SWEEP: 'SWEEP',
TOGGLE_ACTION: 'TOGGLE_ACTION', TOGGLE_ACTION: 'TOGGLE_ACTION',
JUMP_TO_STATE: 'JUMP_TO_STATE', JUMP_TO_STATE: 'JUMP_TO_STATE',
SET_MONITOR_STATE: 'SET_MONITOR_STATE',
RECOMPUTE_STATES: 'RECOMPUTE_STATES' RECOMPUTE_STATES: 'RECOMPUTE_STATES'
}; };
@ -79,15 +78,13 @@ function recomputeStates(reducer, committedState, stagedActions, skippedActions)
/** /**
* Lifts the app state reducer into a DevTools state reducer. * Lifts the app state reducer into a DevTools state reducer.
*/ */
function liftReducer(reducer, initialState) { function liftReducer(reducer, monitorStateReducer, initialState) {
const initialLiftedState = { const initialLiftedState = {
committedState: initialState, committedState: initialState,
stagedActions: [INIT_ACTION], stagedActions: [INIT_ACTION],
skippedActions: {}, skippedActions: {},
currentStateIndex: 0, currentStateIndex: 0,
monitorState: { monitorState: monitorStateReducer(undefined, INIT_ACTION),
isVisible: true
},
timestamps: [Date.now()] timestamps: [Date.now()]
}; };
@ -145,9 +142,6 @@ function liftReducer(reducer, initialState) {
stagedActions = [...stagedActions, liftedAction.action]; stagedActions = [...stagedActions, liftedAction.action];
timestamps = [...timestamps, liftedAction.timestamp]; timestamps = [...timestamps, liftedAction.timestamp];
break; break;
case ActionTypes.SET_MONITOR_STATE:
monitorState = liftedAction.monitorState;
break;
case ActionTypes.RECOMPUTE_STATES: case ActionTypes.RECOMPUTE_STATES:
stagedActions = liftedAction.stagedActions; stagedActions = liftedAction.stagedActions;
timestamps = liftedAction.timestamps; timestamps = liftedAction.timestamps;
@ -166,6 +160,8 @@ function liftReducer(reducer, initialState) {
skippedActions skippedActions
); );
monitorState = monitorStateReducer(monitorState, liftedAction);
return { return {
committedState, committedState,
stagedActions, stagedActions,
@ -202,7 +198,7 @@ function unliftState(liftedState) {
/** /**
* Unlifts the DevTools store to act like the app's store. * Unlifts the DevTools store to act like the app's store.
*/ */
function unliftStore(liftedStore, reducer) { function unliftStore(liftedStore, monitorStateReducer) {
let lastDefinedState; let lastDefinedState;
return { return {
...liftedStore, ...liftedStore,
@ -218,11 +214,8 @@ function unliftStore(liftedStore, reducer) {
} }
return lastDefinedState; return lastDefinedState;
}, },
getReducer() {
return reducer;
},
replaceReducer(nextReducer) { replaceReducer(nextReducer) {
liftedStore.replaceReducer(liftReducer(nextReducer)); liftedStore.replaceReducer(liftReducer(nextReducer, monitorStateReducer));
} }
}; };
} }
@ -249,9 +242,6 @@ export const ActionCreators = {
jumpToState(index) { jumpToState(index) {
return { type: ActionTypes.JUMP_TO_STATE, index }; return { type: ActionTypes.JUMP_TO_STATE, index };
}, },
setMonitorState(monitorState) {
return { type: ActionTypes.SET_MONITOR_STATE, monitorState };
},
recomputeStates(committedState, stagedActions) { recomputeStates(committedState, stagedActions) {
return { return {
type: ActionTypes.RECOMPUTE_STATES, type: ActionTypes.RECOMPUTE_STATES,
@ -264,11 +254,11 @@ export const ActionCreators = {
/** /**
* Redux DevTools middleware. * Redux DevTools middleware.
*/ */
export default function devTools() { export default function devTools(monitorStateReducer = () => undefined) {
return next => (reducer, initialState) => { return next => (reducer, initialState) => {
const liftedReducer = liftReducer(reducer, initialState); const liftedReducer = liftReducer(reducer, monitorStateReducer, initialState);
const liftedStore = next(liftedReducer); const liftedStore = next(liftedReducer);
const store = unliftStore(liftedStore, reducer); const store = unliftStore(liftedStore, monitorStateReducer);
return store; return store;
}; };
} }

View File

@ -11,12 +11,25 @@ class MockMonitor extends Component {
} }
} }
function increment() {
return { type: 'INCREMENT' };
}
function mockMonitorReducer(state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1;
default:
return state;
}
}
describe('connectMonitor', () => { describe('connectMonitor', () => {
jsdom(); jsdom();
it('should pass devToolsStore to monitor', () => { it('should pass devToolsStore to monitor', () => {
const store = devTools()(createStore)(() => {}); const store = devTools()(createStore)(() => {});
const ConnectedMonitor = connectMonitor(MockMonitor); const ConnectedMonitor = connectMonitor()(MockMonitor);
const tree = TestUtils.renderIntoDocument( const tree = TestUtils.renderIntoDocument(
<ConnectedMonitor store={store} /> <ConnectedMonitor store={store} />
); );
@ -26,7 +39,7 @@ describe('connectMonitor', () => {
it('should pass props to monitor', () => { it('should pass props to monitor', () => {
const store = devTools()(createStore)(() => {}); const store = devTools()(createStore)(() => {});
const ConnectedMonitor = connectMonitor(MockMonitor); const ConnectedMonitor = connectMonitor()(MockMonitor);
const tree = TestUtils.renderIntoDocument( const tree = TestUtils.renderIntoDocument(
<ConnectedMonitor store={store} one={1} two={2}/> <ConnectedMonitor store={store} one={1} two={2}/>
); );
@ -36,7 +49,7 @@ describe('connectMonitor', () => {
}); });
it('should subscribe monitor to store updates', () => { it('should subscribe monitor to store updates', () => {
const ConnectedMonitor = connectMonitor(MockMonitor); const ConnectedMonitor = connectMonitor()(MockMonitor);
const store = devTools()(createStore)( const store = devTools()(createStore)(
(state, action) => { (state, action) => {
switch (action.type) { switch (action.type) {
@ -63,7 +76,7 @@ describe('connectMonitor', () => {
it('should warn if devTools() not in middleware', () => { it('should warn if devTools() not in middleware', () => {
const store = createStore(() => {}); const store = createStore(() => {});
const ConnectedMonitor = connectMonitor(MockMonitor); const ConnectedMonitor = connectMonitor()(MockMonitor);
// Force to re-evaluate propType checks on every run // Force to re-evaluate propType checks on every run
ConnectedMonitor.displayName = Math.random().toString(); ConnectedMonitor.displayName = Math.random().toString();
@ -77,4 +90,27 @@ describe('connectMonitor', () => {
(call, errMsg) => call.arguments[0].match(errMsg) (call, errMsg) => call.arguments[0].match(errMsg)
); );
}); });
it('should pass monitor state and actions to the monitor', () => {
const store = devTools(mockMonitorReducer)(createStore)(() => {});
const ConnectedMonitor = connectMonitor()(MockMonitor);
const tree = TestUtils.renderIntoDocument(
<ConnectedMonitor store={store} />
);
const mockMonitor = TestUtils.findRenderedComponentWithType(tree, MockMonitor);
expect(mockMonitor.props.monitorState).toBe(0);
});
it('should pass bound monitor actions to monitor', () => {
const ConnectedMonitor = connectMonitor({ increment })(MockMonitor);
const store = devTools(mockMonitorReducer)(createStore)(() => {});
const tree = TestUtils.renderIntoDocument(
<ConnectedMonitor store={store} />
);
const mockMonitor = TestUtils.findRenderedComponentWithType(tree, MockMonitor);
expect(mockMonitor.props.monitorState).toBe(0);
mockMonitor.props.monitorActions.increment();
expect(mockMonitor.props.monitorState).toBe(1);
});
}); });

View File

@ -143,11 +143,6 @@ describe('devTools', () => {
expect(store.getState()).toBe(2); expect(store.getState()).toBe(2);
}); });
it('should set monitor state', () => {
devToolsStore.dispatch(ActionCreators.setMonitorState({test: 'test'}));
expect(devToolsStore.getState().monitorState.test).toBe('test');
});
it('should recompute', () => { it('should recompute', () => {
store.dispatch({ type: 'INCREMENT' }); store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'DECREMENT' }); store.dispatch({ type: 'DECREMENT' });
@ -167,10 +162,6 @@ describe('devTools', () => {
expect(store.getState()).toBe(13); expect(store.getState()).toBe(13);
}); });
it('should get the reducer', () => {
expect(store.getReducer()).toBe(counter);
});
it('should replace the reducer', () => { it('should replace the reducer', () => {
store.dispatch({ type: 'INCREMENT' }); store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'DECREMENT' }); store.dispatch({ type: 'DECREMENT' });