Explicit > implicit

This commit is contained in:
Dan Abramov 2015-09-28 13:06:05 +03:00
parent b2210f161c
commit 4375d69d5e
9 changed files with 118 additions and 188 deletions

View File

@ -0,0 +1,10 @@
import React from 'react';
import { createDevTools } from 'redux-devtools';
import createLogMonitor from 'redux-devtools-log-monitor';
import createDockMonitor from '../dock/DockMonitor';
export default createDevTools(
createDockMonitor(
createLogMonitor()
)
);

View File

@ -1,24 +1,18 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { DevToolsProvider } from 'redux-devtools';
import CounterApp from './CounterApp'; import CounterApp from './CounterApp';
import LogMonitor from 'redux-devtools-log-monitor'; import DevTools from './DevTools';
import DockMonitor from '../dock/DockMonitor';
export default class Root extends Component { export default class Root extends Component {
render() { render() {
const { store } = this.props; const { store } = this.props;
return ( return (
<div>
<Provider store={store}> <Provider store={store}>
<div>
<CounterApp /> <CounterApp />
</Provider> <DevTools />
<DevToolsProvider store={store}>
<DockMonitor>
<LogMonitor />
</DockMonitor>
</DevToolsProvider>
</div> </div>
</Provider>
); );
} }
} }

View File

@ -4,65 +4,23 @@
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import Dock from 'react-dock'; import Dock from 'react-dock';
import MapProvider from './MapProvider';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { combineReducers } from 'redux'; import { combineReducers, bindActionCreators } 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']; const POSITIONS = ['left', 'top', 'right', 'bottom'];
function wrapReducer(options = {}) { class DockMonitor extends Component {
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 = { static propTypes = {
position: PropTypes.oneOf(['left', 'top', 'right', 'bottom']).isRequired, monitorState: PropTypes.shape({
position: PropTypes.oneOf(POSITIONS).isRequired,
isVisible: PropTypes.bool.isRequired, isVisible: PropTypes.bool.isRequired,
childMonitorState: PropTypes.any, childState: PropTypes.any
}).isRequired,
monitorActions: PropTypes.shape({
toggleVisibility: PropTypes.func.isRequired, toggleVisibility: PropTypes.func.isRequired,
changePosition: PropTypes.func.isRequired changePosition: PropTypes.func.isRequired
}).isRequired
}; };
componentDidMount() { componentDidMount() {
@ -84,10 +42,10 @@ export default class DockMonitor extends Component {
const char = String.fromCharCode(key); const char = String.fromCharCode(key);
switch (char) { switch (char) {
case 'H': case 'H':
this.props.toggleVisibility(); this.props.monitorActions.toggleVisibility();
break; break;
case 'D': case 'D':
this.props.changePosition(); this.props.monitorActions.changePosition();
break; break;
default: default:
break; break;
@ -95,17 +53,75 @@ export default class DockMonitor extends Component {
} }
render() { render() {
const { position, isVisible, children } = this.props; const { children, monitorState } = this.props;
const { position, isVisible } = monitorState;
return ( return (
<Dock position={position} <Dock position={position}
isVisible={isVisible} isVisible={isVisible}
dimMode='none'> dimMode='none'>
<MapProvider mapState={mapUpstreamStateToDownstreamState}>
{children} {children}
</MapProvider>
</Dock> </Dock>
); );
} }
} }
DockMonitor.wrapReducer = wrapReducer; 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 };
}
export default function create(ChildMonitor, {
defaultIsVisible = true,
defaultPosition = 'right'
} = {}) {
function position(state = defaultPosition, action) {
return (action.type === CHANGE_POSITION) ?
POSITIONS[(POSITIONS.indexOf(state) + 1) % POSITIONS.length] :
state;
}
function isVisible(state = defaultIsVisible, action) {
return (action.type === TOGGLE_VISIBILITY) ?
!state :
state;
}
function getChildStore(store) {
return {
...store,
getState() {
const state = store.getState();
return {
...state,
monitorState: state.monitorState.childState
};
}
};
}
const Monitor = connect(
state => state,
dispatch => ({
monitorActions: bindActionCreators({ toggleVisibility, changePosition }, dispatch)
})
)(DockMonitor);
const CompositeMonitor = ({ store }) => (
<Monitor store={store}>
<ChildMonitor store={getChildStore(store)} />
</Monitor>
);
CompositeMonitor.reducer = combineReducers({
childState: ChildMonitor.reducer,
position,
isVisible
});
return CompositeMonitor;
}

View File

@ -1,52 +0,0 @@
//
// TODO: extract to a separate project.
//
import { 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,23 +1,12 @@
import { createStore, applyMiddleware, compose } from 'redux'; import { createStore, applyMiddleware, compose } from 'redux';
import devTools, { persistState } from 'redux-devtools'; import { persistState } from 'redux-devtools';
import thunk from 'redux-thunk'; import thunk from 'redux-thunk';
import rootReducer from '../reducers'; import rootReducer from '../reducers';
import DockMonitor from '../dock/DockMonitor'; import DevTools from '../containers/DevTools';
import LogMonitor from 'redux-devtools-log-monitor';
const finalCreateStore = compose( const finalCreateStore = compose(
applyMiddleware( applyMiddleware(thunk),
thunk DevTools.enhance,
),
devTools(
LogMonitor.createReducer({
preserveScrollTop: true
}),
DockMonitor.wrapReducer({
position: 'right',
isVisible: true
})
),
persistState( persistState(
window.location.href.match( window.location.href.match(
/[?&]debug_session=([^&]+)\b/ /[?&]debug_session=([^&]+)\b/

View File

@ -1,37 +0,0 @@
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>
);
}
}

17
src/createDevTools.js Normal file
View File

@ -0,0 +1,17 @@
import React, { cloneElement, Component, PropTypes } from 'react';
import enhance from './enhance';
export default function createDevTools(Monitor) {
return class DevTools extends Component {
static contextTypes = {
store: PropTypes.object.isRequired
};
static enhance = enhance(Monitor.reducer);
render() {
return <Monitor store={this.context.store.devToolsStore} />;
}
}
}

View File

@ -264,18 +264,11 @@ export const ActionCreators = {
/** /**
* Redux DevTools store enhancer. * Redux DevTools store enhancer.
*/ */
export default function devTools( export default function enhance(monitorReducer = () => null) {
monitorReducer = () => null,
...monitorReducerWrappers
) {
return next => (reducer, initialState) => { return next => (reducer, initialState) => {
const finalMonitorReducer = compose(
...monitorReducerWrappers.slice().reverse()
)(monitorReducer);
const wrapReducer = (r) => combineReducers({ const wrapReducer = (r) => combineReducers({
devToolsState: createDevToolsStateReducer(r, initialState), devToolsState: createDevToolsStateReducer(r, initialState),
monitorState: finalMonitorReducer monitorState: monitorReducer
}); });
const devToolsStore = next(wrapReducer(reducer)); const devToolsStore = next(wrapReducer(reducer));

View File

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