Rewrite to simpler API

This commit is contained in:
Dan Abramov 2015-10-17 02:21:07 +03:00
parent 1f21d83931
commit c23a997a0f
11 changed files with 94 additions and 229 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.2.1",
"react-hot-loader": "^1.3.0", "react-hot-loader": "^1.3.0",
"redux-devtools": "^3.0.0-alpha-8", "redux-devtools": "^3.0.0-beta-1",
"redux-devtools-log-monitor": "^1.0.0-alpha-8", "redux-devtools-log-monitor": "^1.0.0-beta-1",
"redux-devtools-dock-monitor": "^1.0.0-beta-1",
"webpack": "^1.9.11", "webpack": "^1.9.11",
"webpack-dev-server": "^1.9.0" "webpack-dev-server": "^1.9.0"
} }

View File

@ -1,10 +1,11 @@
import React from 'react'; import React from 'react';
import { createDevTools } from 'redux-devtools'; import { createDevTools } from 'redux-devtools';
import LogMonitor from 'redux-devtools-log-monitor'; import LogMonitor from 'redux-devtools-log-monitor';
import DockMonitor from '../dock/DockMonitor'; import DockMonitor from 'redux-devtools-dock-monitor';
export default createDevTools( export default createDevTools(
<DockMonitor> <DockMonitor toggleVisibilityKey='H'
<LogMonitor theme='ocean' /> changePositionKey='Q'>
<LogMonitor />
</DockMonitor> </DockMonitor>
); );

View File

@ -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 (
<Dock position={position}
isVisible={isVisible}
size={size}
onSizeChange={this.handleSizeChange}
dimMode='none'>
{cloneElement(children, childProps)}
</Dock>
);
}
}

View File

@ -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 };
}

View File

@ -1 +0,0 @@
export const POSITIONS = ['left', 'top', 'right', 'bottom'];

View File

@ -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)
};
}

View File

@ -1,6 +1,6 @@
{ {
"name": "redux-devtools", "name": "redux-devtools",
"version": "3.0.0-alpha-8", "version": "3.0.0-beta-1",
"description": "Redux DevTools with hot reloading and time travel", "description": "Redux DevTools with hot reloading and time travel",
"main": "lib/index.js", "main": "lib/index.js",
"scripts": { "scripts": {
@ -43,17 +43,18 @@
"jsdom": "^6.5.1", "jsdom": "^6.5.1",
"mocha": "^2.2.5", "mocha": "^2.2.5",
"mocha-jsdom": "^1.0.0", "mocha-jsdom": "^1.0.0",
"react": "^0.14.0-rc1", "react": "^0.14.0",
"react-addons-test-utils": "^0.14.0-rc1", "react-addons-test-utils": "^0.14.0",
"react-dom": "^0.14.0-rc1", "react-dom": "^0.14.0",
"rimraf": "^2.3.4", "rimraf": "^2.3.4",
"webpack": "^1.11.0" "webpack": "^1.11.0"
}, },
"peerDependencies": { "peerDependencies": {
"redux": "^3.0.0" "redux": "^3.0.0",
"react": "^0.14.0"
}, },
"dependencies": { "dependencies": {
"react-redux": "^3.0.0", "react-redux": "^4.0.0",
"redux": "^3.0.0" "redux": "^3.0.0"
} }
} }

View File

@ -6,19 +6,11 @@ export default function createDevTools(children) {
const monitorElement = Children.only(children); const monitorElement = Children.only(children);
const monitorProps = monitorElement.props; const monitorProps = monitorElement.props;
const Monitor = monitorElement.type; const Monitor = monitorElement.type;
const ConnectedMonitor = connect(state => state)(Monitor);
const enhancer = instrument((state, action) => const enhancer = instrument((state, action) =>
Monitor.reducer(state, action, monitorProps) Monitor.reducer(state, action, monitorProps)
); );
function mapStateToProps(state) {
return {
...state.historyState,
monitorState: state.monitorState
};
}
const ConnectedMonitor = connect(mapStateToProps)(Monitor);
return class DevTools extends Component { return class DevTools extends Component {
static contextTypes = { static contextTypes = {
store: PropTypes.object.isRequired store: PropTypes.object.isRequired
@ -28,13 +20,13 @@ export default function createDevTools(children) {
constructor(props, context) { constructor(props, context) {
super(props, context); super(props, context);
this.instrumentedStore = context.store.instrumentedStore; this.liftedStore = context.store.liftedStore;
} }
render() { render() {
return ( return (
<ConnectedMonitor {...monitorProps} <ConnectedMonitor {...monitorProps}
store={this.instrumentedStore} /> store={this.liftedStore} />
); );
} }
}; };

View File

@ -106,55 +106,57 @@ function recomputeStates(reducer, committedState, stagedActions, skippedActions)
/** /**
* Creates a history state reducer from an app's reducer. * Creates a history state reducer from an app's reducer.
*/ */
function createHistoryReducer(reducer, initialCommittedState) { function liftReducerWith(reducer, initialCommittedState, monitorReducer) {
const initialHistoryState = { const initialLiftedState = {
committedState: initialCommittedState, committedState: initialCommittedState,
stagedActions: [INIT_ACTION], stagedActions: [INIT_ACTION],
skippedActions: {}, skippedActions: {},
currentStateIndex: 0, currentStateIndex: 0,
timestamps: [Date.now()] timestamps: [Date.now()],
monitorState: monitorReducer(undefined, {})
}; };
/** /**
* Manages how the history actions modify the history state. * Manages how the history actions modify the history state.
*/ */
return (historyState = initialHistoryState, historyAction) => { return (liftedState = initialLiftedState, liftedAction) => {
let shouldRecomputeStates = true; let shouldRecomputeStates = true;
let { let {
monitorState,
committedState, committedState,
stagedActions, stagedActions,
skippedActions, skippedActions,
computedStates, computedStates,
currentStateIndex, currentStateIndex,
timestamps timestamps
} = historyState; } = liftedState;
switch (historyAction.type) { switch (liftedAction.type) {
case ActionTypes.RESET: case ActionTypes.RESET:
committedState = initialCommittedState; committedState = initialCommittedState;
stagedActions = [INIT_ACTION]; stagedActions = [INIT_ACTION];
skippedActions = {}; skippedActions = {};
currentStateIndex = 0; currentStateIndex = 0;
timestamps = [historyAction.timestamp]; timestamps = [liftedAction.timestamp];
break; break;
case ActionTypes.COMMIT: case ActionTypes.COMMIT:
committedState = computedStates[currentStateIndex].state; committedState = computedStates[currentStateIndex].state;
stagedActions = [INIT_ACTION]; stagedActions = [INIT_ACTION];
skippedActions = {}; skippedActions = {};
currentStateIndex = 0; currentStateIndex = 0;
timestamps = [historyAction.timestamp]; timestamps = [liftedAction.timestamp];
break; break;
case ActionTypes.ROLLBACK: case ActionTypes.ROLLBACK:
stagedActions = [INIT_ACTION]; stagedActions = [INIT_ACTION];
skippedActions = {}; skippedActions = {};
currentStateIndex = 0; currentStateIndex = 0;
timestamps = [historyAction.timestamp]; timestamps = [liftedAction.timestamp];
break; break;
case ActionTypes.TOGGLE_ACTION: case ActionTypes.TOGGLE_ACTION:
skippedActions = toggle(skippedActions, historyAction.index); skippedActions = toggle(skippedActions, liftedAction.index);
break; break;
case ActionTypes.JUMP_TO_STATE: case ActionTypes.JUMP_TO_STATE:
currentStateIndex = historyAction.index; currentStateIndex = liftedAction.index;
// Optimization: we know the history has not changed. // Optimization: we know the history has not changed.
shouldRecomputeStates = false; shouldRecomputeStates = false;
break; break;
@ -169,8 +171,8 @@ function createHistoryReducer(reducer, initialCommittedState) {
currentStateIndex++; currentStateIndex++;
} }
stagedActions = [...stagedActions, historyAction.action]; stagedActions = [...stagedActions, liftedAction.action];
timestamps = [...timestamps, historyAction.timestamp]; timestamps = [...timestamps, liftedAction.timestamp];
// Optimization: we know that the past has not changed. // Optimization: we know that the past has not changed.
shouldRecomputeStates = false; shouldRecomputeStates = false;
@ -178,13 +180,19 @@ function createHistoryReducer(reducer, initialCommittedState) {
const previousEntry = computedStates[computedStates.length - 1]; const previousEntry = computedStates[computedStates.length - 1];
const nextEntry = computeNextEntry( const nextEntry = computeNextEntry(
reducer, reducer,
historyAction.action, liftedAction.action,
previousEntry.state, previousEntry.state,
previousEntry.error previousEntry.error
); );
computedStates = [...computedStates, nextEntry]; computedStates = [...computedStates, nextEntry];
break; break;
case '@@redux/INIT':
// Always recompute states on hot reload and init.
shouldRecomputeStates = true;
break;
default: default:
// Optimization: a monitor action can't change history.
shouldRecomputeStates = false;
break; break;
} }
@ -197,44 +205,54 @@ function createHistoryReducer(reducer, initialCommittedState) {
); );
} }
monitorState = monitorReducer(monitorState, liftedAction);
return { return {
committedState, committedState,
stagedActions, stagedActions,
skippedActions, skippedActions,
computedStates, computedStates,
currentStateIndex, 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) { function unliftState(liftedState) {
const { computedStates, currentStateIndex } = instrumentedState.historyState; const { computedStates, currentStateIndex } = liftedState;
const { state } = computedStates[currentStateIndex]; const { state } = computedStates[currentStateIndex];
return state; 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; let lastDefinedState;
return { return {
...instrumentedStore, ...liftedStore,
instrumentedStore, liftedStore,
dispatch(action) { dispatch(action) {
instrumentedStore.dispatch(ActionCreators.performAction(action)); liftedStore.dispatch(liftAction(action));
return action; return action;
}, },
getState() { getState() {
const state = selectAppState(instrumentedStore.getState()); const state = unliftState(liftedStore.getState());
if (state !== undefined) { if (state !== undefined) {
lastDefinedState = state; lastDefinedState = state;
} }
@ -242,7 +260,7 @@ function selectAppStore(instrumentedStore, instrumentReducer) {
}, },
replaceReducer(nextReducer) { replaceReducer(nextReducer) {
instrumentedStore.replaceReducer(instrumentReducer(nextReducer)); liftedStore.replaceReducer(liftReducer(nextReducer));
} }
}; };
} }
@ -252,15 +270,11 @@ function selectAppStore(instrumentedStore, instrumentReducer) {
*/ */
export default function instrument(monitorReducer = () => null) { export default function instrument(monitorReducer = () => null) {
return createStore => (reducer, initialState) => { return createStore => (reducer, initialState) => {
function instrumentReducer(r) { function liftReducer(r) {
const historyReducer = createHistoryReducer(r, initialState); return liftReducerWith(r, initialState, monitorReducer);
return ({ historyState, monitorState } = {}, action) => ({
historyState: historyReducer(historyState, action),
monitorState: monitorReducer(monitorState, action)
});
} }
const instrumentedStore = createStore(instrumentReducer(reducer)); const liftedStore = createStore(liftReducer(reducer));
return selectAppStore(instrumentedStore, instrumentReducer); return unliftStore(liftedStore, liftReducer);
}; };
} }

View File

@ -4,18 +4,15 @@ export default function persistState(sessionId, deserializeState = identity, des
return next => (...args) => next(...args); return next => (...args) => next(...args);
} }
function deserialize({ historyState, ...rest }) { function deserialize(state) {
return { return {
...rest, ...state,
historyState: { stagedActions: state.stagedActions.map(deserializeAction),
...historyState, committedState: deserializeState(state.committedState),
stagedActions: historyState.stagedActions.map(deserializeAction), computedStates: state.computedStates.map(computedState => ({
committedState: deserializeState(historyState.committedState), ...computedState,
computedStates: historyState.computedStates.map(computedState => ({ state: deserializeState(computedState.state)
...computedState, }))
state: deserializeState(computedState.state)
}))
}
}; };
} }

View File

@ -29,11 +29,11 @@ function doubleCounter(state = 0, action) {
describe('instrument', () => { describe('instrument', () => {
let store; let store;
let instrumentedStore; let liftedStore;
beforeEach(() => { beforeEach(() => {
store = instrument()(createStore)(counter); store = instrument()(createStore)(counter);
instrumentedStore = store.instrumentedStore; liftedStore = store.liftedStore;
}); });
it('should perform actions', () => { it('should perform actions', () => {
@ -49,20 +49,20 @@ describe('instrument', () => {
store.dispatch({ type: 'INCREMENT' }); store.dispatch({ type: 'INCREMENT' });
expect(store.getState()).toBe(2); expect(store.getState()).toBe(2);
instrumentedStore.dispatch(ActionCreators.commit()); liftedStore.dispatch(ActionCreators.commit());
expect(store.getState()).toBe(2); expect(store.getState()).toBe(2);
store.dispatch({ type: 'INCREMENT' }); store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'INCREMENT' }); store.dispatch({ type: 'INCREMENT' });
expect(store.getState()).toBe(4); expect(store.getState()).toBe(4);
instrumentedStore.dispatch(ActionCreators.rollback()); liftedStore.dispatch(ActionCreators.rollback());
expect(store.getState()).toBe(2); expect(store.getState()).toBe(2);
store.dispatch({ type: 'DECREMENT' }); store.dispatch({ type: 'DECREMENT' });
expect(store.getState()).toBe(1); expect(store.getState()).toBe(1);
instrumentedStore.dispatch(ActionCreators.rollback()); liftedStore.dispatch(ActionCreators.rollback());
expect(store.getState()).toBe(2); expect(store.getState()).toBe(2);
}); });
@ -70,19 +70,19 @@ describe('instrument', () => {
store.dispatch({ type: 'INCREMENT' }); store.dispatch({ type: 'INCREMENT' });
expect(store.getState()).toBe(1); expect(store.getState()).toBe(1);
instrumentedStore.dispatch(ActionCreators.commit()); liftedStore.dispatch(ActionCreators.commit());
expect(store.getState()).toBe(1); expect(store.getState()).toBe(1);
store.dispatch({ type: 'INCREMENT' }); store.dispatch({ type: 'INCREMENT' });
expect(store.getState()).toBe(2); expect(store.getState()).toBe(2);
instrumentedStore.dispatch(ActionCreators.rollback()); liftedStore.dispatch(ActionCreators.rollback());
expect(store.getState()).toBe(1); expect(store.getState()).toBe(1);
store.dispatch({ type: 'INCREMENT' }); store.dispatch({ type: 'INCREMENT' });
expect(store.getState()).toBe(2); expect(store.getState()).toBe(2);
instrumentedStore.dispatch(ActionCreators.reset()); liftedStore.dispatch(ActionCreators.reset());
expect(store.getState()).toBe(0); expect(store.getState()).toBe(0);
}); });
@ -93,10 +93,10 @@ describe('instrument', () => {
store.dispatch({ type: 'INCREMENT' }); store.dispatch({ type: 'INCREMENT' });
expect(store.getState()).toBe(1); expect(store.getState()).toBe(1);
instrumentedStore.dispatch(ActionCreators.toggleAction(2)); liftedStore.dispatch(ActionCreators.toggleAction(2));
expect(store.getState()).toBe(2); expect(store.getState()).toBe(2);
instrumentedStore.dispatch(ActionCreators.toggleAction(2)); liftedStore.dispatch(ActionCreators.toggleAction(2));
expect(store.getState()).toBe(1); expect(store.getState()).toBe(1);
}); });
@ -108,16 +108,16 @@ describe('instrument', () => {
store.dispatch({ type: 'INCREMENT' }); store.dispatch({ type: 'INCREMENT' });
expect(store.getState()).toBe(2); expect(store.getState()).toBe(2);
instrumentedStore.dispatch(ActionCreators.toggleAction(2)); liftedStore.dispatch(ActionCreators.toggleAction(2));
expect(store.getState()).toBe(3); expect(store.getState()).toBe(3);
instrumentedStore.dispatch(ActionCreators.sweep()); liftedStore.dispatch(ActionCreators.sweep());
expect(store.getState()).toBe(3); expect(store.getState()).toBe(3);
instrumentedStore.dispatch(ActionCreators.toggleAction(2)); liftedStore.dispatch(ActionCreators.toggleAction(2));
expect(store.getState()).toBe(2); expect(store.getState()).toBe(2);
instrumentedStore.dispatch(ActionCreators.sweep()); liftedStore.dispatch(ActionCreators.sweep());
expect(store.getState()).toBe(2); expect(store.getState()).toBe(2);
}); });
@ -127,19 +127,19 @@ describe('instrument', () => {
store.dispatch({ type: 'INCREMENT' }); store.dispatch({ type: 'INCREMENT' });
expect(store.getState()).toBe(1); expect(store.getState()).toBe(1);
instrumentedStore.dispatch(ActionCreators.jumpToState(0)); liftedStore.dispatch(ActionCreators.jumpToState(0));
expect(store.getState()).toBe(0); expect(store.getState()).toBe(0);
instrumentedStore.dispatch(ActionCreators.jumpToState(1)); liftedStore.dispatch(ActionCreators.jumpToState(1));
expect(store.getState()).toBe(1); expect(store.getState()).toBe(1);
instrumentedStore.dispatch(ActionCreators.jumpToState(2)); liftedStore.dispatch(ActionCreators.jumpToState(2));
expect(store.getState()).toBe(0); expect(store.getState()).toBe(0);
store.dispatch({ type: 'INCREMENT' }); store.dispatch({ type: 'INCREMENT' });
expect(store.getState()).toBe(0); expect(store.getState()).toBe(0);
instrumentedStore.dispatch(ActionCreators.jumpToState(4)); liftedStore.dispatch(ActionCreators.jumpToState(4));
expect(store.getState()).toBe(2); expect(store.getState()).toBe(2);
}); });
@ -161,11 +161,11 @@ describe('instrument', () => {
storeWithBug.dispatch({ type: 'DECREMENT' }); storeWithBug.dispatch({ type: 'DECREMENT' });
storeWithBug.dispatch({ type: 'INCREMENT' }); storeWithBug.dispatch({ type: 'INCREMENT' });
let historyState = storeWithBug.instrumentedStore.getState().historyState; let { computedStates } = storeWithBug.liftedStore.getState();
expect(historyState.computedStates[2].error).toMatch( expect(computedStates[2].error).toMatch(
/ReferenceError/ /ReferenceError/
); );
expect(historyState.computedStates[3].error).toMatch( expect(computedStates[3].error).toMatch(
/Interrupted by an error up the chain/ /Interrupted by an error up the chain/
); );
expect(spy.calls[0].arguments[0]).toMatch( expect(spy.calls[0].arguments[0]).toMatch(
@ -198,7 +198,7 @@ describe('instrument', () => {
it('should not recompute states when jumping to state', () => { it('should not recompute states when jumping to state', () => {
let reducerCalls = 0; let reducerCalls = 0;
let monitoredStore = instrument()(createStore)(() => reducerCalls++); let monitoredStore = instrument()(createStore)(() => reducerCalls++);
let monitoredInstrumentedStore = monitoredStore.instrumentedStore; let monitoredInstrumentedStore = monitoredStore.liftedStore;
expect(reducerCalls).toBe(1); expect(reducerCalls).toBe(1);
monitoredStore.dispatch({ type: 'INCREMENT' }); monitoredStore.dispatch({ type: 'INCREMENT' });