Figure out monitor composition

This commit is contained in:
Dan Abramov 2015-09-28 03:52:10 +03:00
parent 1edf2d25c8
commit c1256ed8ff
9 changed files with 277 additions and 78 deletions

View File

@ -28,8 +28,8 @@
"node-libs-browser": "^0.5.2",
"react-dock": "^0.1.0",
"react-hot-loader": "^1.3.0",
"redux-devtools": "^3.0.0-alpha-1",
"redux-devtools-log-monitor": "^1.0.0-alpha-3",
"redux-devtools": "^3.0.0-alpha-4",
"redux-devtools-log-monitor": "^1.0.0-alpha-4",
"webpack": "^1.9.11",
"webpack-dev-server": "^1.9.0"
}

View File

@ -1,8 +1,9 @@
import React, { Component } from 'react';
import { Provider } from 'react-redux';
import { DevToolsProvider } from 'redux-devtools';
import CounterApp from './CounterApp';
import Dock from 'react-dock';
import LogMonitor from 'redux-devtools-log-monitor';
import DockMonitor from '../dock/DockMonitor';
export default class Root extends Component {
render() {
@ -12,9 +13,11 @@ export default class Root extends Component {
<Provider store={store}>
<CounterApp />
</Provider>
<Dock position='right' isVisible dimMode='none'>
<LogMonitor store={store} />
</Dock>
<DevToolsProvider store={store}>
<DockMonitor>
<LogMonitor />
</DockMonitor>
</DevToolsProvider>
</div>
);
}

View File

@ -0,0 +1,109 @@
//
// TODO: extract to a separate project.
//
import React, { Component, PropTypes } from 'react';
import Dock from 'react-dock';
import MapProvider from './MapProvider';
import { connect } from 'react-redux';
import { combineReducers } from 'redux';
const TOGGLE_VISIBILITY = '@@redux-devtools/dock/TOGGLE_VISIBILITY';
function toggleVisibility() {
return { type: TOGGLE_VISIBILITY };
}
const CHANGE_POSITION = '@@redux-devtools/dock/CHANGE_POSITION';
function changePosition() {
return { type: CHANGE_POSITION };
}
const POSITIONS = ['left', 'top', 'right', 'bottom'];
function wrapReducer(options = {}) {
const {
isVisible: initialIsVisible = true,
position: initialPosition = 'right'
} = options;
function position(state = initialPosition, action) {
return (action.type === CHANGE_POSITION) ?
POSITIONS[(POSITIONS.indexOf(state) + 1) % POSITIONS.length] :
state;
}
function isVisible(state = initialIsVisible, action) {
return (action.type === TOGGLE_VISIBILITY) ?
!state :
state;
}
return childMonitorReducer => combineReducers({
childMonitorState: childMonitorReducer,
position,
isVisible
});
}
function mapUpstreamStateToDownstreamState(state) {
return {
devToolsState: state.devToolsState,
monitorState: state.monitorState.childMonitorState
};
}
@connect(
state => state.monitorState,
{ toggleVisibility, changePosition }
)
export default class DockMonitor extends Component {
static propTypes = {
position: PropTypes.oneOf(['left', 'top', 'right', 'bottom']).isRequired,
isVisible: PropTypes.bool.isRequired,
childMonitorState: PropTypes.any,
toggleVisibility: PropTypes.func.isRequired,
changePosition: PropTypes.func.isRequired
};
componentDidMount() {
this.handleKeyDown = this.handleKeyDown.bind(this);
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) {
case 'H':
this.props.toggleVisibility();
break;
case 'D':
this.props.changePosition();
break;
}
}
render() {
const { position, isVisible, children } = this.props;
return (
<Dock position={position}
isVisible={isVisible}
dimMode='none'>
<MapProvider mapState={mapUpstreamStateToDownstreamState}>
{children}
</MapProvider>
</Dock>
);
}
}
DockMonitor.wrapReducer = wrapReducer;

View File

@ -0,0 +1,52 @@
//
// TODO: extract to a separate project.
//
import React, { Children, Component, PropTypes } from 'react';
const identity = _ => _;
function mapStore(store, { mapAction = identity, mapState = identity }) {
return {
...store,
dispatch(action) {
return store.dispatch(mapAction(action));
},
subscribe(listener) {
return store.subscribe(listener);
},
getState() {
return mapState(store.getState());
}
};
}
export default class MapProvider extends Component {
static propTypes = {
mapAction: PropTypes.func,
mapState: PropTypes.func
};
static contextTypes = {
store: PropTypes.object.isRequired
};
static childContextTypes = {
store: PropTypes.object.isRequired
};
getChildContext() {
return {
store: this.store
};
}
constructor(props, context) {
super(props, context);
this.store = mapStore(context.store, props);
}
render() {
return Children.only(this.props.children);
}
}

View File

@ -1,15 +1,28 @@
import { createStore, applyMiddleware, compose } from 'redux';
import { devTools, persistState } from 'redux-devtools';
import devTools, { persistState } from 'redux-devtools';
import thunk from 'redux-thunk';
import rootReducer from '../reducers';
import { createMonitorReducer } from 'redux-devtools-log-monitor';
import DockMonitor from '../dock/DockMonitor';
import LogMonitor from 'redux-devtools-log-monitor';
const finalCreateStore = compose(
applyMiddleware(thunk),
devTools(createMonitorReducer({
isVisibleOnLoad: true
})),
persistState(window.location.href.match(/[?&]debug_session=([^&]+)\b/))
applyMiddleware(
thunk
),
devTools(
LogMonitor.createReducer({
preserveScrollTop: true
}),
DockMonitor.wrapReducer({
position: 'right',
isVisible: true
})
),
persistState(
window.location.href.match(
/[?&]debug_session=([^&]+)\b/
)
)
)(createStore);
export default function configureStore(initialState) {

37
src/DevToolsProvider.js Normal file
View File

@ -0,0 +1,37 @@
import React, { Component } from 'react';
import { Provider } from 'react-redux';
export default class DevToolsProvider extends Component {
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, children } = this.props;
if (!store) {
return null;
}
const { devToolsStore } = store;
if (!devToolsStore) {
return null;
}
return (
<Provider store={devToolsStore}>
{children}
</Provider>
);
}
}

View File

@ -16,38 +16,6 @@ export default function connectMonitor(monitorActionCreators = {}) {
}
const ConnectedMonitor = connect(mapStateToProps, mapDispatchToProps)(Monitor);
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

@ -1,3 +1,5 @@
import { combineReducers, compose } from 'redux';
const ActionTypes = {
PERFORM_ACTION: 'PERFORM_ACTION',
RESET: 'RESET',
@ -77,20 +79,19 @@ function recomputeStates(reducer, committedState, stagedActions, skippedActions)
/**
* Lifts the app state reducer into a DevTools state reducer.
*/
function liftReducer(reducer, monitorReducer, initialState) {
const initialLiftedState = {
committedState: initialState,
function createDevToolsStateReducer(reducer, initialCommittedState) {
const initialState = {
committedState: initialCommittedState,
stagedActions: [INIT_ACTION],
skippedActions: {},
currentStateIndex: 0,
monitorState: monitorReducer(undefined, INIT_ACTION),
timestamps: [Date.now()]
};
/**
* Manages how the DevTools actions modify the DevTools state.
*/
return function liftedReducer(liftedState = initialLiftedState, liftedAction) {
return function devToolsState(state = initialState, action) {
let shouldRecomputeStates = true;
let {
committedState,
@ -98,36 +99,35 @@ function liftReducer(reducer, monitorReducer, initialState) {
skippedActions,
computedStates,
currentStateIndex,
monitorState,
timestamps
} = liftedState;
} = state;
switch (liftedAction.type) {
switch (action.type) {
case ActionTypes.RESET:
committedState = initialState;
stagedActions = [INIT_ACTION];
skippedActions = {};
currentStateIndex = 0;
timestamps = [liftedAction.timestamp];
timestamps = [action.timestamp];
break;
case ActionTypes.COMMIT:
committedState = computedStates[currentStateIndex].state;
stagedActions = [INIT_ACTION];
skippedActions = {};
currentStateIndex = 0;
timestamps = [liftedAction.timestamp];
timestamps = [action.timestamp];
break;
case ActionTypes.ROLLBACK:
stagedActions = [INIT_ACTION];
skippedActions = {};
currentStateIndex = 0;
timestamps = [liftedAction.timestamp];
timestamps = [action.timestamp];
break;
case ActionTypes.TOGGLE_ACTION:
skippedActions = toggle(skippedActions, liftedAction.index);
skippedActions = toggle(skippedActions, action.index);
break;
case ActionTypes.JUMP_TO_STATE:
currentStateIndex = liftedAction.index;
currentStateIndex = action.index;
// Optimization: we know the history has not changed.
shouldRecomputeStates = false;
break;
@ -142,8 +142,8 @@ function liftReducer(reducer, monitorReducer, initialState) {
currentStateIndex++;
}
stagedActions = [...stagedActions, liftedAction.action];
timestamps = [...timestamps, liftedAction.timestamp];
stagedActions = [...stagedActions, action.action];
timestamps = [...timestamps, action.timestamp];
// Optimization: we know that the past has not changed.
shouldRecomputeStates = false;
@ -151,7 +151,7 @@ function liftReducer(reducer, monitorReducer, initialState) {
const previousEntry = computedStates[computedStates.length - 1];
const nextEntry = computeNextEntry(
reducer,
liftedAction.action,
action.action,
previousEntry.state,
previousEntry.error
);
@ -170,15 +170,12 @@ function liftReducer(reducer, monitorReducer, initialState) {
);
}
monitorState = monitorReducer(monitorState, liftedAction);
return {
committedState,
stagedActions,
skippedActions,
computedStates,
currentStateIndex,
monitorState,
timestamps
};
};
@ -200,7 +197,7 @@ function liftAction(action) {
* Unlifts the DevTools state to the app state.
*/
function unliftState(liftedState) {
const { computedStates, currentStateIndex } = liftedState;
const { computedStates, currentStateIndex } = liftedState.devToolsState;
const { state } = computedStates[currentStateIndex];
return state;
}
@ -208,24 +205,29 @@ function unliftState(liftedState) {
/**
* Unlifts the DevTools store to act like the app's store.
*/
function unliftStore(liftedStore, monitorReducer) {
function mapToComputedStateStore(devToolsStore, wrapReducer) {
let lastDefinedState;
return {
...liftedStore,
devToolsStore: liftedStore,
...devToolsStore,
devToolsStore,
dispatch(action) {
liftedStore.dispatch(liftAction(action));
devToolsStore.dispatch(liftAction(action));
return action;
},
getState() {
const state = unliftState(liftedStore.getState());
const state = unliftState(devToolsStore.getState());
if (state !== undefined) {
lastDefinedState = state;
}
return lastDefinedState;
},
replaceReducer(nextReducer) {
liftedStore.replaceReducer(liftReducer(nextReducer, monitorReducer));
devToolsStore.replaceReducer(wrapReducer(nextReducer));
}
};
}
@ -237,18 +239,23 @@ export const ActionCreators = {
reset() {
return { type: ActionTypes.RESET, timestamp: Date.now() };
},
rollback() {
return { type: ActionTypes.ROLLBACK, timestamp: Date.now() };
},
commit() {
return { type: ActionTypes.COMMIT, timestamp: Date.now() };
},
sweep() {
return { type: ActionTypes.SWEEP };
},
toggleAction(index) {
return { type: ActionTypes.TOGGLE_ACTION, index };
},
jumpToState(index) {
return { type: ActionTypes.JUMP_TO_STATE, index };
}
@ -257,11 +264,21 @@ export const ActionCreators = {
/**
* Redux DevTools store enhancer.
*/
export default function devTools(monitorReducer = () => undefined) {
export default function devTools(
monitorReducer = () => null,
...monitorReducerWrappers
) {
return next => (reducer, initialState) => {
const liftedReducer = liftReducer(reducer, monitorReducer, initialState);
const liftedStore = next(liftedReducer);
const store = unliftStore(liftedStore, monitorReducer);
return store;
const finalMonitorReducer = compose(
...monitorReducerWrappers.slice().reverse()
)(monitorReducer);
const wrapReducer = (r) => combineReducers({
devToolsState: createDevToolsStateReducer(r, initialState),
monitorState: finalMonitorReducer
});
const devToolsStore = next(wrapReducer(reducer));
return mapToComputedStateStore(devToolsStore, wrapReducer);
};
}

View File

@ -1,3 +1,3 @@
export { default as devTools } from './devTools';
export { default, ActionCreators, ActionTypes } from './devTools';
export { default as DevToolsProvider } from './DevToolsProvider';
export { default as persistState } from './persistState';
export { default as connectMonitor } from './connectMonitor';