diff --git a/examples/counter/package.json b/examples/counter/package.json index cbc4daa6..539ff539 100644 --- a/examples/counter/package.json +++ b/examples/counter/package.json @@ -26,10 +26,10 @@ "babel-core": "^5.6.18", "babel-loader": "^5.1.4", "node-libs-browser": "^0.5.2", - "react-dock": "^0.2.1", "react-hot-loader": "^1.3.0", - "redux-devtools": "^3.0.0-alpha-8", - "redux-devtools-log-monitor": "^1.0.0-alpha-8", + "redux-devtools": "^3.0.0-beta-1", + "redux-devtools-log-monitor": "^1.0.0-beta-1", + "redux-devtools-dock-monitor": "^1.0.0-beta-1", "webpack": "^1.9.11", "webpack-dev-server": "^1.9.0" } diff --git a/examples/counter/src/containers/DevTools.js b/examples/counter/src/containers/DevTools.js index 7a574375..70fd5bf4 100644 --- a/examples/counter/src/containers/DevTools.js +++ b/examples/counter/src/containers/DevTools.js @@ -1,10 +1,11 @@ import React from 'react'; import { createDevTools } from 'redux-devtools'; import LogMonitor from 'redux-devtools-log-monitor'; -import DockMonitor from '../dock/DockMonitor'; +import DockMonitor from 'redux-devtools-dock-monitor'; export default createDevTools( - - + + ); diff --git a/examples/counter/src/dock/DockMonitor.js b/examples/counter/src/dock/DockMonitor.js deleted file mode 100644 index 19db0893..00000000 --- a/examples/counter/src/dock/DockMonitor.js +++ /dev/null @@ -1,91 +0,0 @@ -import React, { cloneElement, Component, PropTypes } from 'react'; -import Dock from 'react-dock'; -import { POSITIONS } from './constants'; -import { toggleVisibility, changePosition, changeSize } from './actions'; -import reducer from './reducers'; - -export default class DockMonitor extends Component { - static reducer = reducer; - - static propTypes = { - defaultPosition: PropTypes.oneOf(POSITIONS).isRequired, - defaultIsVisible: PropTypes.bool.isRequired, - defaultSize: PropTypes.number.isRequired, - toggleVisibilityShortcut: PropTypes.string.isRequired, - changePositionShortcut: PropTypes.string.isRequired, - children: PropTypes.element, - - dispatch: PropTypes.func, - monitorState: PropTypes.shape({ - position: PropTypes.oneOf(POSITIONS).isRequired, - size: PropTypes.number.isRequired, - isVisible: PropTypes.bool.isRequired, - childMonitorState: PropTypes.any - }) - }; - - static defaultProps = { - defaultIsVisible: true, - defaultPosition: 'right', - defaultSize: 0.3, - toggleVisibilityShortcut: 'H', - changePositionShortcut: 'Q' - }; - - constructor(props) { - super(props); - this.handleKeyDown = this.handleKeyDown.bind(this); - this.handleSizeChange = this.handleSizeChange.bind(this); - } - - componentDidMount() { - window.addEventListener('keydown', this.handleKeyDown); - } - - componentWillUnmount() { - window.removeEventListener('keydown', this.handleKeyDown); - } - - handleKeyDown(e) { - if (!e.ctrlKey) { - return; - } - e.preventDefault(); - - const key = event.keyCode || event.which; - const char = String.fromCharCode(key); - switch (char.toUpperCase()) { - case this.props.toggleVisibilityShortcut.toUpperCase(): - this.props.dispatch(toggleVisibility()); - break; - case this.props.changePositionShortcut.toUpperCase(): - this.props.dispatch(changePosition()); - break; - default: - break; - } - } - - handleSizeChange(requestedSize) { - this.props.dispatch(changeSize(requestedSize)); - } - - render() { - const { monitorState, children, ...rest } = this.props; - const { position, isVisible, size } = monitorState; - const childProps = { - ...rest, - monitorState: monitorState.childMonitorState - }; - - return ( - - {cloneElement(children, childProps)} - - ); - } -} diff --git a/examples/counter/src/dock/actions.js b/examples/counter/src/dock/actions.js deleted file mode 100644 index 4a743666..00000000 --- a/examples/counter/src/dock/actions.js +++ /dev/null @@ -1,14 +0,0 @@ -export const TOGGLE_VISIBILITY = '@@redux-devtools/dock/TOGGLE_VISIBILITY'; -export function toggleVisibility() { - return { type: TOGGLE_VISIBILITY }; -} - -export const CHANGE_POSITION = '@@redux-devtools/dock/CHANGE_POSITION'; -export function changePosition() { - return { type: CHANGE_POSITION }; -} - -export const CHANGE_SIZE = '@@redux-devtools/dock/CHANGE_SIZE'; -export function changeSize(size) { - return { type: CHANGE_SIZE, size: size }; -} diff --git a/examples/counter/src/dock/constants.js b/examples/counter/src/dock/constants.js deleted file mode 100644 index e935427f..00000000 --- a/examples/counter/src/dock/constants.js +++ /dev/null @@ -1 +0,0 @@ -export const POSITIONS = ['left', 'top', 'right', 'bottom']; diff --git a/examples/counter/src/dock/reducers.js b/examples/counter/src/dock/reducers.js deleted file mode 100644 index 42b7726d..00000000 --- a/examples/counter/src/dock/reducers.js +++ /dev/null @@ -1,34 +0,0 @@ -import { CHANGE_POSITION, CHANGE_SIZE, TOGGLE_VISIBILITY } from './actions'; -import { POSITIONS } from './constants'; - -function position(state = props.defaultPosition, action, props) { - return (action.type === CHANGE_POSITION) ? - POSITIONS[(POSITIONS.indexOf(state) + 1) % POSITIONS.length] : - state; -} - -function size(state = props.defaultSize, action, props) { - return (action.type === CHANGE_SIZE) ? - action.size : - state; -} - -function isVisible(state = props.defaultIsVisible, action, props) { - return (action.type === TOGGLE_VISIBILITY) ? - !state : - state; -} - -function childMonitorState(state, action, props) { - const child = props.children; - return child.type.reducer(state, action, child.props); -} - -export default function reducer(state = {}, action, props) { - return { - position: position(state.position, action, props), - isVisible: isVisible(state.isVisible, action, props), - size: size(state.size, action, props), - childMonitorState: childMonitorState(state.childMonitorState, action, props) - }; -} diff --git a/package.json b/package.json index a73ffc2a..3829c032 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "redux-devtools", - "version": "3.0.0-alpha-8", + "version": "3.0.0-beta-1", "description": "Redux DevTools with hot reloading and time travel", "main": "lib/index.js", "scripts": { @@ -43,17 +43,18 @@ "jsdom": "^6.5.1", "mocha": "^2.2.5", "mocha-jsdom": "^1.0.0", - "react": "^0.14.0-rc1", - "react-addons-test-utils": "^0.14.0-rc1", - "react-dom": "^0.14.0-rc1", + "react": "^0.14.0", + "react-addons-test-utils": "^0.14.0", + "react-dom": "^0.14.0", "rimraf": "^2.3.4", "webpack": "^1.11.0" }, "peerDependencies": { - "redux": "^3.0.0" + "redux": "^3.0.0", + "react": "^0.14.0" }, "dependencies": { - "react-redux": "^3.0.0", + "react-redux": "^4.0.0", "redux": "^3.0.0" } } diff --git a/src/createDevTools.js b/src/createDevTools.js index 7935ec52..44730567 100644 --- a/src/createDevTools.js +++ b/src/createDevTools.js @@ -6,19 +6,11 @@ export default function createDevTools(children) { const monitorElement = Children.only(children); const monitorProps = monitorElement.props; const Monitor = monitorElement.type; - + const ConnectedMonitor = connect(state => state)(Monitor); const enhancer = instrument((state, action) => Monitor.reducer(state, action, monitorProps) ); - function mapStateToProps(state) { - return { - ...state.historyState, - monitorState: state.monitorState - }; - } - const ConnectedMonitor = connect(mapStateToProps)(Monitor); - return class DevTools extends Component { static contextTypes = { store: PropTypes.object.isRequired @@ -28,13 +20,13 @@ export default function createDevTools(children) { constructor(props, context) { super(props, context); - this.instrumentedStore = context.store.instrumentedStore; + this.liftedStore = context.store.liftedStore; } render() { return ( + store={this.liftedStore} /> ); } }; diff --git a/src/instrument.js b/src/instrument.js index 735b0f66..3c292395 100644 --- a/src/instrument.js +++ b/src/instrument.js @@ -106,55 +106,57 @@ function recomputeStates(reducer, committedState, stagedActions, skippedActions) /** * Creates a history state reducer from an app's reducer. */ -function createHistoryReducer(reducer, initialCommittedState) { - const initialHistoryState = { +function liftReducerWith(reducer, initialCommittedState, monitorReducer) { + const initialLiftedState = { committedState: initialCommittedState, stagedActions: [INIT_ACTION], skippedActions: {}, currentStateIndex: 0, - timestamps: [Date.now()] + timestamps: [Date.now()], + monitorState: monitorReducer(undefined, {}) }; /** * Manages how the history actions modify the history state. */ - return (historyState = initialHistoryState, historyAction) => { + return (liftedState = initialLiftedState, liftedAction) => { let shouldRecomputeStates = true; let { + monitorState, committedState, stagedActions, skippedActions, computedStates, currentStateIndex, timestamps - } = historyState; + } = liftedState; - switch (historyAction.type) { + switch (liftedAction.type) { case ActionTypes.RESET: committedState = initialCommittedState; stagedActions = [INIT_ACTION]; skippedActions = {}; currentStateIndex = 0; - timestamps = [historyAction.timestamp]; + timestamps = [liftedAction.timestamp]; break; case ActionTypes.COMMIT: committedState = computedStates[currentStateIndex].state; stagedActions = [INIT_ACTION]; skippedActions = {}; currentStateIndex = 0; - timestamps = [historyAction.timestamp]; + timestamps = [liftedAction.timestamp]; break; case ActionTypes.ROLLBACK: stagedActions = [INIT_ACTION]; skippedActions = {}; currentStateIndex = 0; - timestamps = [historyAction.timestamp]; + timestamps = [liftedAction.timestamp]; break; case ActionTypes.TOGGLE_ACTION: - skippedActions = toggle(skippedActions, historyAction.index); + skippedActions = toggle(skippedActions, liftedAction.index); break; case ActionTypes.JUMP_TO_STATE: - currentStateIndex = historyAction.index; + currentStateIndex = liftedAction.index; // Optimization: we know the history has not changed. shouldRecomputeStates = false; break; @@ -169,8 +171,8 @@ function createHistoryReducer(reducer, initialCommittedState) { currentStateIndex++; } - stagedActions = [...stagedActions, historyAction.action]; - timestamps = [...timestamps, historyAction.timestamp]; + stagedActions = [...stagedActions, liftedAction.action]; + timestamps = [...timestamps, liftedAction.timestamp]; // Optimization: we know that the past has not changed. shouldRecomputeStates = false; @@ -178,13 +180,19 @@ function createHistoryReducer(reducer, initialCommittedState) { const previousEntry = computedStates[computedStates.length - 1]; const nextEntry = computeNextEntry( reducer, - historyAction.action, + liftedAction.action, previousEntry.state, previousEntry.error ); computedStates = [...computedStates, nextEntry]; break; + case '@@redux/INIT': + // Always recompute states on hot reload and init. + shouldRecomputeStates = true; + break; default: + // Optimization: a monitor action can't change history. + shouldRecomputeStates = false; break; } @@ -197,44 +205,54 @@ function createHistoryReducer(reducer, initialCommittedState) { ); } + monitorState = monitorReducer(monitorState, liftedAction); + return { committedState, stagedActions, skippedActions, computedStates, currentStateIndex, - timestamps + timestamps, + monitorState }; }; } /** - * Provides a view into the History state that matches the current app state. + * Provides an app's view into the state of the lifted store. */ -function selectAppState(instrumentedState) { - const { computedStates, currentStateIndex } = instrumentedState.historyState; +function unliftState(liftedState) { + const { computedStates, currentStateIndex } = liftedState; const { state } = computedStates[currentStateIndex]; return state; } /** - * Deinstruments the History store to act like the app's store. + * Lifts an app's action into an action on the lifted store. */ -function selectAppStore(instrumentedStore, instrumentReducer) { +function liftAction(action) { + return ActionCreators.performAction(action); +} + +/** + * Provides an app's view into the lifted store. + */ +function unliftStore(liftedStore, liftReducer) { let lastDefinedState; return { - ...instrumentedStore, + ...liftedStore, - instrumentedStore, + liftedStore, dispatch(action) { - instrumentedStore.dispatch(ActionCreators.performAction(action)); + liftedStore.dispatch(liftAction(action)); return action; }, getState() { - const state = selectAppState(instrumentedStore.getState()); + const state = unliftState(liftedStore.getState()); if (state !== undefined) { lastDefinedState = state; } @@ -242,7 +260,7 @@ function selectAppStore(instrumentedStore, instrumentReducer) { }, replaceReducer(nextReducer) { - instrumentedStore.replaceReducer(instrumentReducer(nextReducer)); + liftedStore.replaceReducer(liftReducer(nextReducer)); } }; } @@ -252,15 +270,11 @@ function selectAppStore(instrumentedStore, instrumentReducer) { */ export default function instrument(monitorReducer = () => null) { return createStore => (reducer, initialState) => { - function instrumentReducer(r) { - const historyReducer = createHistoryReducer(r, initialState); - return ({ historyState, monitorState } = {}, action) => ({ - historyState: historyReducer(historyState, action), - monitorState: monitorReducer(monitorState, action) - }); + function liftReducer(r) { + return liftReducerWith(r, initialState, monitorReducer); } - const instrumentedStore = createStore(instrumentReducer(reducer)); - return selectAppStore(instrumentedStore, instrumentReducer); + const liftedStore = createStore(liftReducer(reducer)); + return unliftStore(liftedStore, liftReducer); }; } diff --git a/src/persistState.js b/src/persistState.js index 0aa61e89..8dafa36e 100644 --- a/src/persistState.js +++ b/src/persistState.js @@ -4,18 +4,15 @@ export default function persistState(sessionId, deserializeState = identity, des return next => (...args) => next(...args); } - function deserialize({ historyState, ...rest }) { + function deserialize(state) { return { - ...rest, - historyState: { - ...historyState, - stagedActions: historyState.stagedActions.map(deserializeAction), - committedState: deserializeState(historyState.committedState), - computedStates: historyState.computedStates.map(computedState => ({ - ...computedState, - state: deserializeState(computedState.state) - })) - } + ...state, + stagedActions: state.stagedActions.map(deserializeAction), + committedState: deserializeState(state.committedState), + computedStates: state.computedStates.map(computedState => ({ + ...computedState, + state: deserializeState(computedState.state) + })) }; } diff --git a/test/instrument.spec.js b/test/instrument.spec.js index 542dd1c5..34577b04 100644 --- a/test/instrument.spec.js +++ b/test/instrument.spec.js @@ -29,11 +29,11 @@ function doubleCounter(state = 0, action) { describe('instrument', () => { let store; - let instrumentedStore; + let liftedStore; beforeEach(() => { store = instrument()(createStore)(counter); - instrumentedStore = store.instrumentedStore; + liftedStore = store.liftedStore; }); it('should perform actions', () => { @@ -49,20 +49,20 @@ describe('instrument', () => { store.dispatch({ type: 'INCREMENT' }); expect(store.getState()).toBe(2); - instrumentedStore.dispatch(ActionCreators.commit()); + liftedStore.dispatch(ActionCreators.commit()); expect(store.getState()).toBe(2); store.dispatch({ type: 'INCREMENT' }); store.dispatch({ type: 'INCREMENT' }); expect(store.getState()).toBe(4); - instrumentedStore.dispatch(ActionCreators.rollback()); + liftedStore.dispatch(ActionCreators.rollback()); expect(store.getState()).toBe(2); store.dispatch({ type: 'DECREMENT' }); expect(store.getState()).toBe(1); - instrumentedStore.dispatch(ActionCreators.rollback()); + liftedStore.dispatch(ActionCreators.rollback()); expect(store.getState()).toBe(2); }); @@ -70,19 +70,19 @@ describe('instrument', () => { store.dispatch({ type: 'INCREMENT' }); expect(store.getState()).toBe(1); - instrumentedStore.dispatch(ActionCreators.commit()); + liftedStore.dispatch(ActionCreators.commit()); expect(store.getState()).toBe(1); store.dispatch({ type: 'INCREMENT' }); expect(store.getState()).toBe(2); - instrumentedStore.dispatch(ActionCreators.rollback()); + liftedStore.dispatch(ActionCreators.rollback()); expect(store.getState()).toBe(1); store.dispatch({ type: 'INCREMENT' }); expect(store.getState()).toBe(2); - instrumentedStore.dispatch(ActionCreators.reset()); + liftedStore.dispatch(ActionCreators.reset()); expect(store.getState()).toBe(0); }); @@ -93,10 +93,10 @@ describe('instrument', () => { store.dispatch({ type: 'INCREMENT' }); expect(store.getState()).toBe(1); - instrumentedStore.dispatch(ActionCreators.toggleAction(2)); + liftedStore.dispatch(ActionCreators.toggleAction(2)); expect(store.getState()).toBe(2); - instrumentedStore.dispatch(ActionCreators.toggleAction(2)); + liftedStore.dispatch(ActionCreators.toggleAction(2)); expect(store.getState()).toBe(1); }); @@ -108,16 +108,16 @@ describe('instrument', () => { store.dispatch({ type: 'INCREMENT' }); expect(store.getState()).toBe(2); - instrumentedStore.dispatch(ActionCreators.toggleAction(2)); + liftedStore.dispatch(ActionCreators.toggleAction(2)); expect(store.getState()).toBe(3); - instrumentedStore.dispatch(ActionCreators.sweep()); + liftedStore.dispatch(ActionCreators.sweep()); expect(store.getState()).toBe(3); - instrumentedStore.dispatch(ActionCreators.toggleAction(2)); + liftedStore.dispatch(ActionCreators.toggleAction(2)); expect(store.getState()).toBe(2); - instrumentedStore.dispatch(ActionCreators.sweep()); + liftedStore.dispatch(ActionCreators.sweep()); expect(store.getState()).toBe(2); }); @@ -127,19 +127,19 @@ describe('instrument', () => { store.dispatch({ type: 'INCREMENT' }); expect(store.getState()).toBe(1); - instrumentedStore.dispatch(ActionCreators.jumpToState(0)); + liftedStore.dispatch(ActionCreators.jumpToState(0)); expect(store.getState()).toBe(0); - instrumentedStore.dispatch(ActionCreators.jumpToState(1)); + liftedStore.dispatch(ActionCreators.jumpToState(1)); expect(store.getState()).toBe(1); - instrumentedStore.dispatch(ActionCreators.jumpToState(2)); + liftedStore.dispatch(ActionCreators.jumpToState(2)); expect(store.getState()).toBe(0); store.dispatch({ type: 'INCREMENT' }); expect(store.getState()).toBe(0); - instrumentedStore.dispatch(ActionCreators.jumpToState(4)); + liftedStore.dispatch(ActionCreators.jumpToState(4)); expect(store.getState()).toBe(2); }); @@ -161,11 +161,11 @@ describe('instrument', () => { storeWithBug.dispatch({ type: 'DECREMENT' }); storeWithBug.dispatch({ type: 'INCREMENT' }); - let historyState = storeWithBug.instrumentedStore.getState().historyState; - expect(historyState.computedStates[2].error).toMatch( + let { computedStates } = storeWithBug.liftedStore.getState(); + expect(computedStates[2].error).toMatch( /ReferenceError/ ); - expect(historyState.computedStates[3].error).toMatch( + expect(computedStates[3].error).toMatch( /Interrupted by an error up the chain/ ); expect(spy.calls[0].arguments[0]).toMatch( @@ -198,7 +198,7 @@ describe('instrument', () => { it('should not recompute states when jumping to state', () => { let reducerCalls = 0; let monitoredStore = instrument()(createStore)(() => reducerCalls++); - let monitoredInstrumentedStore = monitoredStore.instrumentedStore; + let monitoredInstrumentedStore = monitoredStore.liftedStore; expect(reducerCalls).toBe(1); monitoredStore.dispatch({ type: 'INCREMENT' });