From b6d21bbea6dd9e3d909f4c223fa54400442e95d7 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Wed, 8 Jul 2015 16:58:55 +0300 Subject: [PATCH 01/14] Start working on bringing devtools to 1.0 API (WIP) --- examples/counter/containers/App.js | 29 +++- examples/counter/redux-devtools/DebugPanel.js | 46 ++++++ .../counter/redux-devtools/ReduxMonitor.js | 155 ++++++++++++++++++ .../redux-devtools/ReduxMonitorEntry.js | 118 +++++++++++++ examples/counter/redux-devtools/TODO | 3 + examples/counter/redux-devtools/devtools.js | 54 ++++++ .../counter/redux-devtools/devtools_old.js | 115 +++++++++++++ src/Store.js | 4 +- 8 files changed, 515 insertions(+), 9 deletions(-) create mode 100644 examples/counter/redux-devtools/DebugPanel.js create mode 100644 examples/counter/redux-devtools/ReduxMonitor.js create mode 100644 examples/counter/redux-devtools/ReduxMonitorEntry.js create mode 100644 examples/counter/redux-devtools/TODO create mode 100644 examples/counter/redux-devtools/devtools.js create mode 100644 examples/counter/redux-devtools/devtools_old.js diff --git a/examples/counter/containers/App.js b/examples/counter/containers/App.js index edca2dd1f2..4e6755f063 100644 --- a/examples/counter/containers/App.js +++ b/examples/counter/containers/App.js @@ -1,9 +1,13 @@ import React, { Component } from 'react'; import CounterApp from './CounterApp'; -import { createStore, applyMiddleware, combineReducers } from 'redux'; -import { Provider } from 'react-redux'; +import { createStore, applyMiddleware, compose, combineReducers } from 'redux'; +import { Provider } from 'redux/react'; import * as reducers from '../reducers'; +import DebugPanel from '../redux-devtools/DebugPanel'; +import devtools from '../redux-devtools/devtools'; +import ReduxMonitor from '../redux-devtools/ReduxMonitor'; + // TODO: move into a separate project function thunk({ dispatch, getState }) { return next => action => @@ -12,16 +16,27 @@ function thunk({ dispatch, getState }) { next(action); } -const createStoreWithMiddleware = applyMiddleware(thunk)(createStore); +const finalCreateStore = compose( + applyMiddleware(), + devtools(), + createStore +); + const reducer = combineReducers(reducers); -const store = createStoreWithMiddleware(reducer); +const store = finalCreateStore(combineReducers(reducers)); +const devToolsStore = store.getDevToolsStore(); export default class App extends Component { render() { return ( - - {() => } - +
+ + {() => } + + + {() => } + +
); } } diff --git a/examples/counter/redux-devtools/DebugPanel.js b/examples/counter/redux-devtools/DebugPanel.js new file mode 100644 index 0000000000..8c0b289007 --- /dev/null +++ b/examples/counter/redux-devtools/DebugPanel.js @@ -0,0 +1,46 @@ +import React, { PropTypes } from 'react'; + +export default class DebugPanel { + static propTypes = { + left: PropTypes.bool, + right: PropTypes.bool, + bottom: PropTypes.bool, + top: PropTypes.bool + }; + + render() { + if (process.env.NODE_ENV === 'production') { + return null; + } + + let { left, right, bottom, top } = this.props; + if (typeof left === 'undefined' && typeof right === 'undefined') { + right = true; + } + if (typeof top === 'undefined' && typeof bottom === 'undefined') { + bottom = true; + } + + return ( +
+ {this.props.children} +
+ ); + } +} diff --git a/examples/counter/redux-devtools/ReduxMonitor.js b/examples/counter/redux-devtools/ReduxMonitor.js new file mode 100644 index 0000000000..0aa4fb7f89 --- /dev/null +++ b/examples/counter/redux-devtools/ReduxMonitor.js @@ -0,0 +1,155 @@ +import React, { PropTypes, findDOMNode } from 'react'; +import { ActionTypes } from './devtools'; +import { connect } from '../../../src/react'; +import ReduxMonitorEntry from './ReduxMonitorEntry'; +import identity from 'lodash/utility/identity'; +import values from 'lodash/object/values'; + +@connect(state => ({ + actions: state.actions || [], // TODO + states: state.states || [], // TODO + disabledActions: state.disabledActions || {}, // TODO + error: state.error || null // TODO +})) +export default class ReduxMonitor { + static propTypes = { + actions: PropTypes.array.isRequired, + states: PropTypes.array.isRequired, + select: PropTypes.func.isRequired + }; + + static defaultProps = { + select: identity + }; + + componentWillReceiveProps(nextProps) { + if (this.props.actions.length < nextProps.actions.length) { + const scrollableNode = findDOMNode(this).parentElement; + const { scrollTop, offsetHeight, scrollHeight } = scrollableNode; + + this.scrollDown = Math.abs( + scrollHeight - (scrollTop + offsetHeight) + ) < 20; + } else { + this.scrollDown = false; + } + } + + componentDidUpdate(prevProps) { + if ( + prevProps.actions.length < this.props.actions.length && + this.scrollDown + ) { + const scrollableNode = findDOMNode(this).parentElement; + const { scrollTop, offsetHeight, scrollHeight } = scrollableNode; + + scrollableNode.scrollTop = scrollHeight - offsetHeight; + this.scrollDown = false; + } + } + + handleRollback() { + this.props.dispatch({ + type: ActionTypes.ROLLBACK + }); + } + + handleSweep() { + this.props.dispatch({ + type: ActionTypes.SWEEP + }); + } + + handleCommit() { + this.props.dispatch({ + type: ActionTypes.COMMIT + }); + } + + handleToggleAction(index, toggleMany) { + this.props.dispatch({ + type: ActionTypes.TOGGLE_ACTION, + index, + toggleMany + }); + } + + handleReset() { + this.props.dispatch({ + type: ActionTypes.RESET + }); + } + + render() { + const elements = []; + const { actions, disabledActions, states, error, select } = this.props; + + for (let i = 0; i < actions.length; i++) { + const action = actions[i]; + const state = states[i]; + + let errorText; + if (error) { + if (error.index === i) { + errorText = error.text; + } else if (error.index < i) { + errorText = 'Interrupted by an error up the chain.'; + } + } + + elements.push( + + ); + } + + return ( +
+
+ + Reset + +
+ {elements} +
+ {actions.length > 1 && + + Rollback + + } + {values(disabledActions).some(identity) && + + {' • '} + + Sweep + + + } + {actions.length > 1 && + + + {' • '} + + + Commit + + + } +
+
+ ); + } +} diff --git a/examples/counter/redux-devtools/ReduxMonitorEntry.js b/examples/counter/redux-devtools/ReduxMonitorEntry.js new file mode 100644 index 0000000000..00d5459306 --- /dev/null +++ b/examples/counter/redux-devtools/ReduxMonitorEntry.js @@ -0,0 +1,118 @@ +import React, { PropTypes } from 'react'; + +function hsvToRgb(h, s, v) { + const i = Math.floor(h); + const f = h - i; + const p = v * (1 - s); + const q = v * (1 - f * s); + const t = v * (1 - (1 - f) * s); + const mod = i % 6; + const r = [v, q, p, p, t, v][mod]; + const g = [t, v, v, q, p, p][mod]; + const b = [p, p, t, v, v, q][mod]; + + return { + r: Math.round(r * 255), + g: Math.round(g * 255), + b: Math.round(b * 255) + }; +} + +function colorFromString(token) { + token = token.split(''); + token = token.concat(token.reverse()); + const number = token + .reduce((sum, char) => sum + char.charCodeAt(0), 0) * + Math.abs(Math.sin(token.length)); + + const h = Math.round((number * (180 / Math.PI) * token.length) % 360); + const s = number % 100 / 100; + const v = 1; + + return hsvToRgb(h, s, v); +} + + +export default class ReduxMonitorEntry { + static propTypes = { + index: PropTypes.number.isRequired, + state: PropTypes.object.isRequired, + action: PropTypes.object.isRequired, + select: PropTypes.func.isRequired, + errorText: PropTypes.string, + onActionClick: PropTypes.func.isRequired, + collapsed: PropTypes.bool + }; + + printState(state, errorText) { + if (!errorText) { + try { + return JSON.stringify(this.props.select(state)); + } catch (err) { + errorText = 'Error selecting state.'; + } + } + + return ( + + ({errorText}) + + ); + } + + handleActionClick(e) { + const { index, onActionClick } = this.props; + onActionClick(index, e.ctrlKey || e.metaKey); + } + + render() { + const { index, errorText, action, state, collapsed, onActionClick } = this.props; + const { type = '' } = action; + const { r, g, b } = colorFromString(action.type); + + return ( +
+ + {JSON.stringify(action)} + + + {!collapsed && +

+ ⇧ +

+ } + + {!collapsed && +
+ {this.printState(state, errorText)} +
+ } + +
+
+ ); + } +} diff --git a/examples/counter/redux-devtools/TODO b/examples/counter/redux-devtools/TODO new file mode 100644 index 0000000000..0ac3cb8ef2 --- /dev/null +++ b/examples/counter/redux-devtools/TODO @@ -0,0 +1,3 @@ +* separate state +* localStorage +* initial state \ No newline at end of file diff --git a/examples/counter/redux-devtools/devtools.js b/examples/counter/redux-devtools/devtools.js new file mode 100644 index 0000000000..6ebe90c297 --- /dev/null +++ b/examples/counter/redux-devtools/devtools.js @@ -0,0 +1,54 @@ +export const ActionTypes = { + PERFORM: 'PERFORM' +}; + +function lift(reducer) { + const initialState = { + appState: reducer(undefined, { type: '@@INIT' }) + }; + + return function handleDevToolsAction(state = initialState, action) { + switch (action.type) { + case ActionTypes.PERFORM: + return { + ...state, + appState: reducer(state.appState, action.action) + }; + default: + return state; + } + }; +} + +function unlift(store) { + function getState() { + return store.getState().appState; + } + + function dispatch(action) { + store.dispatch({ + type: ActionTypes.PERFORM, + action + }); + } + + return { + ...store, + dispatch, + getState, + getDevToolsStore() { + return store; + } + }; +} + +export default function devtools() { + // TODO: initial state + return next => reducer => { + const devToolsReducer = lift(reducer); + const devToolsStore = next(devToolsReducer); + const store = unlift(devToolsStore); + + return store; + }; +} diff --git a/examples/counter/redux-devtools/devtools_old.js b/examples/counter/redux-devtools/devtools_old.js new file mode 100644 index 0000000000..8bb3d9c293 --- /dev/null +++ b/examples/counter/redux-devtools/devtools_old.js @@ -0,0 +1,115 @@ +export const StateKeys = { + ACTIONS: '@@actions', + DISABLED_ACTIONS: '@@disabledActions', + STATES: '@@states', + INITIAL_STATE: '@@initialState', + ERROR: '@@error' +}; + +export const ActionTypes = { + INIT: '@@INIT', + ROLLBACK: '@@ROLLBACK', + SWEEP: '@@SWEEP', + COMMIT: '@@COMMIT', + TOGGLE_ACTION: '@@TOGGLE_ACTION', + JUMP_TO_STATE: '@@JUMP_TO_STATE', + RESET: '@@RESET' +}; + +export default function monitor(store) { + return function Recorder(state = {}, action) { + let { + [StateKeys.ACTIONS]: actions = [], + [StateKeys.DISABLED_ACTIONS]: disabledActions = {}, + [StateKeys.STATES]: atoms = [], + [StateKeys.INITIAL_STATE]: initialAtom = state, + [StateKeys.ERROR]: error = null, + ...atom + } = state; + + switch (action.type) { + case ActionTypes.RESET: + return Recorder({}, { type: ActionTypes.INIT }); + case ActionTypes.COMMIT: + actions = []; + disabledActions = {}; + atoms = []; + initialAtom = atom; + break; + case ActionTypes.ROLLBACK: + actions = []; + disabledActions = {}; + atoms = []; + initialAtom = initialAtom; // sic + break; + case ActionTypes.SWEEP: + actions = actions.filter((_, i) => !disabledActions[i]); + atoms = atoms.filter((_, i) => !disabledActions[i]); + disabledActions = {}; + break; + case ActionTypes.TOGGLE_ACTION: + if (action.toggleMany) { + disabledActions = {}; + for (let i = 0; i < actions.length; i++) { + disabledActions[i] = i > action.index; + } + } else { + disabledActions = { + ...disabledActions, + [action.index]: !disabledActions[action.index] + }; + } + break; + } + + error = null; + actions = [...actions, action]; + atoms = actions.reduce((allAtoms, nextAction) => { + const index = allAtoms.length; + const prevAtom = index > 0 ? + allAtoms[index - 1] : + initialAtom; + + if (error || disabledActions[index] === true) { + allAtoms.push(prevAtom); + } else { + try { + allAtoms.push(store(prevAtom, nextAction)); + } catch (err) { + error = { + index: index, + text: err.toString() + }; + console.error(err); + allAtoms.push(prevAtom); + } + } + + return allAtoms; + }, []); + const nextAtom = atoms[atoms.length - 1]; + + switch (action.type) { + case ActionTypes.INIT: + if (atoms.length > 1) { + atoms.pop(); + actions.pop(); + } + break; + case ActionTypes.TOGGLE_ACTION: + case ActionTypes.SWEEP: + atoms.pop(); + actions.pop(); + break; + } + + return { + [StateKeys.ACTIONS]: actions, + [StateKeys.DISABLED_ACTIONS]: disabledActions, + [StateKeys.STATES]: atoms, + [StateKeys.INITIAL_STATE]: initialAtom, + [StateKeys.ERROR]: error, + ...nextAtom + }; + }; +} diff --git a/src/Store.js b/src/Store.js index ed85b987fc..8a1ea59c75 100644 --- a/src/Store.js +++ b/src/Store.js @@ -2,13 +2,13 @@ import invariant from 'invariant'; import isPlainObject from './utils/isPlainObject'; export default class Store { - constructor(reducer, initialState) { + constructor(reducer) { invariant( typeof reducer === 'function', 'Expected the reducer to be a function.' ); - this.state = initialState; + this.state = undefined; this.listeners = []; this.replaceReducer(reducer); } From b46d266e81432aae839542f881b38248aa46d51e Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Thu, 9 Jul 2015 00:58:26 +0300 Subject: [PATCH 02/14] It sort of works --- examples/counter/containers/App.js | 7 ++- .../counter/redux-devtools/ReduxMonitor.js | 21 +++---- examples/counter/redux-devtools/devtools.js | 56 ++++++++++--------- src/Store.js | 4 +- 4 files changed, 47 insertions(+), 41 deletions(-) diff --git a/examples/counter/containers/App.js b/examples/counter/containers/App.js index 4e6755f063..0139a271fb 100644 --- a/examples/counter/containers/App.js +++ b/examples/counter/containers/App.js @@ -33,8 +33,11 @@ export default class App extends Component { {() => } - - {() => } + + + + {() => } + ); diff --git a/examples/counter/redux-devtools/ReduxMonitor.js b/examples/counter/redux-devtools/ReduxMonitor.js index 0aa4fb7f89..0dbca3905e 100644 --- a/examples/counter/redux-devtools/ReduxMonitor.js +++ b/examples/counter/redux-devtools/ReduxMonitor.js @@ -6,15 +6,13 @@ import identity from 'lodash/utility/identity'; import values from 'lodash/object/values'; @connect(state => ({ - actions: state.actions || [], // TODO - states: state.states || [], // TODO + log: state.log || [], // TODO disabledActions: state.disabledActions || {}, // TODO error: state.error || null // TODO })) export default class ReduxMonitor { static propTypes = { - actions: PropTypes.array.isRequired, - states: PropTypes.array.isRequired, + log: PropTypes.array.isRequired, select: PropTypes.func.isRequired }; @@ -23,7 +21,7 @@ export default class ReduxMonitor { }; componentWillReceiveProps(nextProps) { - if (this.props.actions.length < nextProps.actions.length) { + if (this.props.log.length < nextProps.log.length) { const scrollableNode = findDOMNode(this).parentElement; const { scrollTop, offsetHeight, scrollHeight } = scrollableNode; @@ -37,7 +35,7 @@ export default class ReduxMonitor { componentDidUpdate(prevProps) { if ( - prevProps.actions.length < this.props.actions.length && + prevProps.log.length < this.props.log.length && this.scrollDown ) { const scrollableNode = findDOMNode(this).parentElement; @@ -82,11 +80,10 @@ export default class ReduxMonitor { render() { const elements = []; - const { actions, disabledActions, states, error, select } = this.props; + const { disabledActions, log, error, select } = this.props; - for (let i = 0; i < actions.length; i++) { - const action = actions[i]; - const state = states[i]; + for (let i = 0; i < log.length; i++) { + const { action, state } = log[i]; let errorText; if (error) { @@ -122,7 +119,7 @@ export default class ReduxMonitor { {elements}
- {actions.length > 1 && + {log.length > 1 && Rollback @@ -137,7 +134,7 @@ export default class ReduxMonitor { } - {actions.length > 1 && + {log.length > 1 && {' • '} diff --git a/examples/counter/redux-devtools/devtools.js b/examples/counter/redux-devtools/devtools.js index 6ebe90c297..35518ab1bc 100644 --- a/examples/counter/redux-devtools/devtools.js +++ b/examples/counter/redux-devtools/devtools.js @@ -1,54 +1,60 @@ export const ActionTypes = { - PERFORM: 'PERFORM' + PERFORM_ACTION: 'PERFORM_ACTION' }; -function lift(reducer) { - const initialState = { - appState: reducer(undefined, { type: '@@INIT' }) +const INIT_ACTION = { type: '@@INIT' }; + +function wrap(reducer, initialState = reducer(undefined, INIT_ACTION)) { + const initialDevState = { + state: initialState, + log: [{ state: initialState, action: INIT_ACTION }] + }; + + const handlers = { + [ActionTypes.PERFORM_ACTION]({ log, state }, { action }) { + state = reducer(state, action); + log = [...log, { state, action }]; + return { state, log }; + } }; - return function handleDevToolsAction(state = initialState, action) { - switch (action.type) { - case ActionTypes.PERFORM: - return { - ...state, - appState: reducer(state.appState, action.action) - }; - default: - return state; + return function handleDevAction(devState = initialDevState, devAction) { + if (!handlers.hasOwnProperty(devAction.type)) { + return devState; } + + const nextDevState = handlers[devAction.type](devState, devAction); + return { ...devState, ...nextDevState }; }; } -function unlift(store) { +function unwrap(devStore) { function getState() { - return store.getState().appState; + return devStore.getState().state; } function dispatch(action) { - store.dispatch({ - type: ActionTypes.PERFORM, + devStore.dispatch({ + type: ActionTypes.PERFORM_ACTION, action }); } return { - ...store, + ...devStore, dispatch, getState, getDevToolsStore() { - return store; + return devStore; } }; } export default function devtools() { - // TODO: initial state - return next => reducer => { - const devToolsReducer = lift(reducer); - const devToolsStore = next(devToolsReducer); - const store = unlift(devToolsStore); - + return next => (reducer, initialState) => { + const devReducer = wrap(reducer, initialState); + const devStore = next(devReducer); + const store = unwrap(devStore); return store; }; } diff --git a/src/Store.js b/src/Store.js index 8a1ea59c75..ed85b987fc 100644 --- a/src/Store.js +++ b/src/Store.js @@ -2,13 +2,13 @@ import invariant from 'invariant'; import isPlainObject from './utils/isPlainObject'; export default class Store { - constructor(reducer) { + constructor(reducer, initialState) { invariant( typeof reducer === 'function', 'Expected the reducer to be a function.' ); - this.state = undefined; + this.state = initialState; this.listeners = []; this.replaceReducer(reducer); } From fcb5e155f23c7bfce6329dc06b756a628498a28a Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Thu, 9 Jul 2015 01:12:14 +0300 Subject: [PATCH 03/14] Minor refactoring --- examples/counter/redux-devtools/devtools.js | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/examples/counter/redux-devtools/devtools.js b/examples/counter/redux-devtools/devtools.js index 35518ab1bc..ae23ee3b73 100644 --- a/examples/counter/redux-devtools/devtools.js +++ b/examples/counter/redux-devtools/devtools.js @@ -1,5 +1,6 @@ export const ActionTypes = { - PERFORM_ACTION: 'PERFORM_ACTION' + PERFORM_ACTION: 'PERFORM_ACTION', + RESET: 'RESET' }; const INIT_ACTION = { type: '@@INIT' }; @@ -10,12 +11,19 @@ function wrap(reducer, initialState = reducer(undefined, INIT_ACTION)) { log: [{ state: initialState, action: INIT_ACTION }] }; + function performAction({ log, state }, { action }) { + state = reducer(state, action); + log = [...log, { state, action }]; + return { state, log }; + } + + function reset() { + return initialDevState; + } + const handlers = { - [ActionTypes.PERFORM_ACTION]({ log, state }, { action }) { - state = reducer(state, action); - log = [...log, { state, action }]; - return { state, log }; - } + [ActionTypes.RESET]: reset, + [ActionTypes.PERFORM_ACTION]: performAction }; return function handleDevAction(devState = initialDevState, devAction) { From 18834baa07ddc142ecab5454c50bcc1786d4289d Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Fri, 10 Jul 2015 20:30:03 +0300 Subject: [PATCH 04/14] Implement replaying and error handling --- .../counter/redux-devtools/ReduxMonitor.js | 34 ++--- examples/counter/redux-devtools/devtools.js | 143 +++++++++++++----- 2 files changed, 118 insertions(+), 59 deletions(-) diff --git a/examples/counter/redux-devtools/ReduxMonitor.js b/examples/counter/redux-devtools/ReduxMonitor.js index 0dbca3905e..8311a6eed1 100644 --- a/examples/counter/redux-devtools/ReduxMonitor.js +++ b/examples/counter/redux-devtools/ReduxMonitor.js @@ -6,13 +6,13 @@ import identity from 'lodash/utility/identity'; import values from 'lodash/object/values'; @connect(state => ({ - log: state.log || [], // TODO - disabledActions: state.disabledActions || {}, // TODO - error: state.error || null // TODO + actions: state.actions, + computations: state.computations, + disabledActions: state.disabledActions || {} // TODO })) export default class ReduxMonitor { static propTypes = { - log: PropTypes.array.isRequired, + computations: PropTypes.array.isRequired, select: PropTypes.func.isRequired }; @@ -21,7 +21,7 @@ export default class ReduxMonitor { }; componentWillReceiveProps(nextProps) { - if (this.props.log.length < nextProps.log.length) { + if (this.props.computations.length < nextProps.computations.length) { const scrollableNode = findDOMNode(this).parentElement; const { scrollTop, offsetHeight, scrollHeight } = scrollableNode; @@ -35,7 +35,7 @@ export default class ReduxMonitor { componentDidUpdate(prevProps) { if ( - prevProps.log.length < this.props.log.length && + prevProps.computations.length < this.props.computations.length && this.scrollDown ) { const scrollableNode = findDOMNode(this).parentElement; @@ -80,19 +80,11 @@ export default class ReduxMonitor { render() { const elements = []; - const { disabledActions, log, error, select } = this.props; + const { disabledActions, actions, computations, select } = this.props; - for (let i = 0; i < log.length; i++) { - const { action, state } = log[i]; - - let errorText; - if (error) { - if (error.index === i) { - errorText = error.text; - } else if (error.index < i) { - errorText = 'Interrupted by an error up the chain.'; - } - } + for (let i = 0; i < actions.length; i++) { + const action = actions[i]; + const { state, error } = computations[i]; elements.push( ); } @@ -119,7 +111,7 @@ export default class ReduxMonitor {
{elements}
- {log.length > 1 && + {computations.length > 1 && Rollback @@ -134,7 +126,7 @@ export default class ReduxMonitor { } - {log.length > 1 && + {computations.length > 1 && {' • '} diff --git a/examples/counter/redux-devtools/devtools.js b/examples/counter/redux-devtools/devtools.js index ae23ee3b73..87eee95daa 100644 --- a/examples/counter/redux-devtools/devtools.js +++ b/examples/counter/redux-devtools/devtools.js @@ -3,66 +3,133 @@ export const ActionTypes = { RESET: 'RESET' }; -const INIT_ACTION = { type: '@@INIT' }; +const INIT_ACTION = { + type: '@@INIT' +}; -function wrap(reducer, initialState = reducer(undefined, INIT_ACTION)) { - const initialDevState = { - state: initialState, - log: [{ state: initialState, action: INIT_ACTION }] - }; +/** + * Computes the next entry in the log by applying an action. + */ +function computeNextEntry(reducer, action, state, error) { + if (error) { + return { + state, + error: 'Interrupted by an error up the chain' + }; + } - function performAction({ log, state }, { action }) { + try { state = reducer(state, action); - log = [...log, { state, action }]; - return { state, log }; + } catch (err) { + error = err.toString(); } - function reset() { - return initialDevState; + return { state, error }; +} + +/** + * Runs the reducer on all actions to get a fresh computation log. + * It's probably a good idea to do this only if the code has changed, + * but until we have some tests we'll just do it every time an action fires. + */ +function recompute(reducer, liftedState) { + const { initialState, actions } = liftedState; + const computations = []; + + for (let i = 0; i < actions.length; i++) { + const action = actions[i]; + + const previousEntry = computations[i - 1]; + const previousState = previousEntry ? previousEntry.state : undefined; + const previousError = previousEntry ? previousEntry.error : undefined; + + const entry = computeNextEntry(reducer, action, previousState, previousError); + computations.push(entry); } - const handlers = { - [ActionTypes.RESET]: reset, - [ActionTypes.PERFORM_ACTION]: performAction + return computations; +} + + +/** + * Lifts the app state reducer into a DevTools state reducer. + */ +function liftReducer(reducer, initialState) { + const initialLiftedState = { + initialState, + actions: [INIT_ACTION] }; - return function handleDevAction(devState = initialDevState, devAction) { - if (!handlers.hasOwnProperty(devAction.type)) { - return devState; + /** + * Manages how the DevTools actions modify the DevTools state. + */ + return function liftedReducer(liftedState = initialLiftedState, liftedAction) { + switch (liftedAction.type) { + case ActionTypes.RESET: + liftedState = initialLiftedState; + break; + case ActionTypes.PERFORM_ACTION: + const { actions } = liftedState; + const { action } = liftedAction; + liftedState = { + ...liftedState, + actions: [...actions, action] + }; + break; } - const nextDevState = handlers[devAction.type](devState, devAction); - return { ...devState, ...nextDevState }; + return { + ...liftedState, + computations: recompute(reducer, liftedState) + }; }; } -function unwrap(devStore) { - function getState() { - return devStore.getState().state; - } +/** + * Lifts an app action to a DevTools action. + */ +function liftAction(action) { + const liftedAction = { type: ActionTypes.PERFORM_ACTION, action }; + return liftedAction; +} - function dispatch(action) { - devStore.dispatch({ - type: ActionTypes.PERFORM_ACTION, - action - }); - } +/** + * Unlifts the DevTools state to the app state. + */ +function unliftState(liftedState) { + const { computations } = liftedState; + const lastComputation = computations[computations.length - 1]; + const { state } = lastComputation; + return state; +} - return { - ...devStore, - dispatch, - getState, +/** + * Unlifts the DevTools store to act like the app's store. + */ +function unliftStore(liftedStore) { + const store = { + ...liftedStore, + dispatch(action) { + liftedStore.dispatch(liftAction(action)); + }, + getState() { + return unliftState(liftedStore.getState()); + }, getDevToolsStore() { - return devStore; + return liftedStore; } }; + return store; } -export default function devtools() { +/** + * Redux DevTools middleware. + */ +export default function devTools() { return next => (reducer, initialState) => { - const devReducer = wrap(reducer, initialState); - const devStore = next(devReducer); - const store = unwrap(devStore); + const liftedReducer = liftReducer(reducer, initialState); + const liftedStore = next(liftedReducer); + const store = unliftStore(liftedStore); return store; }; } From c98a90248da2ede74490a18fbe25f2d4b1ce7f97 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Fri, 10 Jul 2015 20:53:08 +0300 Subject: [PATCH 05/14] Commit and rollback your pending actions --- examples/counter/redux-devtools/devtools.js | 33 +++++++++++++++++---- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/examples/counter/redux-devtools/devtools.js b/examples/counter/redux-devtools/devtools.js index 87eee95daa..cbb9fbe9bb 100644 --- a/examples/counter/redux-devtools/devtools.js +++ b/examples/counter/redux-devtools/devtools.js @@ -1,12 +1,18 @@ export const ActionTypes = { PERFORM_ACTION: 'PERFORM_ACTION', - RESET: 'RESET' + RESET: 'RESET', + ROLLBACK: 'ROLLBACK', + COMMIT: 'COMMIT' }; const INIT_ACTION = { type: '@@INIT' }; +function last(arr) { + return arr[arr.length - 1]; +} + /** * Computes the next entry in the log by applying an action. */ @@ -40,7 +46,7 @@ function recompute(reducer, liftedState) { const action = actions[i]; const previousEntry = computations[i - 1]; - const previousState = previousEntry ? previousEntry.state : undefined; + const previousState = previousEntry ? previousEntry.state : initialState; const previousError = previousEntry ? previousEntry.error : undefined; const entry = computeNextEntry(reducer, action, previousState, previousError); @@ -66,7 +72,25 @@ function liftReducer(reducer, initialState) { return function liftedReducer(liftedState = initialLiftedState, liftedAction) { switch (liftedAction.type) { case ActionTypes.RESET: - liftedState = initialLiftedState; + liftedState = { + ...liftedState, + actions: [INIT_ACTION], + initialState + }; + break; + case ActionTypes.COMMIT: + const { computations } = liftedState; + liftedState = { + ...liftedState, + actions: [INIT_ACTION], + initialState: last(computations).state + }; + break; + case ActionTypes.ROLLBACK: + liftedState = { + ...liftedState, + actions: [INIT_ACTION] + }; break; case ActionTypes.PERFORM_ACTION: const { actions } = liftedState; @@ -98,8 +122,7 @@ function liftAction(action) { */ function unliftState(liftedState) { const { computations } = liftedState; - const lastComputation = computations[computations.length - 1]; - const { state } = lastComputation; + const { state } = last(computations); return state; } From 2a66bced89533bd20ed3701ebded9cc0bdbc4711 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Fri, 10 Jul 2015 21:03:53 +0300 Subject: [PATCH 06/14] Make it easier to understand --- .../counter/redux-devtools/ReduxMonitor.js | 8 ++-- examples/counter/redux-devtools/devtools.js | 48 +++++++------------ 2 files changed, 20 insertions(+), 36 deletions(-) diff --git a/examples/counter/redux-devtools/ReduxMonitor.js b/examples/counter/redux-devtools/ReduxMonitor.js index 8311a6eed1..9fe515d575 100644 --- a/examples/counter/redux-devtools/ReduxMonitor.js +++ b/examples/counter/redux-devtools/ReduxMonitor.js @@ -6,7 +6,7 @@ import identity from 'lodash/utility/identity'; import values from 'lodash/object/values'; @connect(state => ({ - actions: state.actions, + stagedActions: state.stagedActions, computations: state.computations, disabledActions: state.disabledActions || {} // TODO })) @@ -80,10 +80,10 @@ export default class ReduxMonitor { render() { const elements = []; - const { disabledActions, actions, computations, select } = this.props; + const { disabledActions, stagedActions, computations, select } = this.props; - for (let i = 0; i < actions.length; i++) { - const action = actions[i]; + for (let i = 0; i < stagedActions.length; i++) { + const action = stagedActions[i]; const { state, error } = computations[i]; elements.push( diff --git a/examples/counter/redux-devtools/devtools.js b/examples/counter/redux-devtools/devtools.js index cbb9fbe9bb..9a170b28da 100644 --- a/examples/counter/redux-devtools/devtools.js +++ b/examples/counter/redux-devtools/devtools.js @@ -38,15 +38,14 @@ function computeNextEntry(reducer, action, state, error) { * It's probably a good idea to do this only if the code has changed, * but until we have some tests we'll just do it every time an action fires. */ -function recompute(reducer, liftedState) { - const { initialState, actions } = liftedState; +function recompute(reducer, committedState, stagedActions) { const computations = []; - for (let i = 0; i < actions.length; i++) { - const action = actions[i]; + for (let i = 0; i < stagedActions.length; i++) { + const action = stagedActions[i]; const previousEntry = computations[i - 1]; - const previousState = previousEntry ? previousEntry.state : initialState; + const previousState = previousEntry ? previousEntry.state : committedState; const previousError = previousEntry ? previousEntry.error : undefined; const entry = computeNextEntry(reducer, action, previousState, previousError); @@ -62,50 +61,35 @@ function recompute(reducer, liftedState) { */ function liftReducer(reducer, initialState) { const initialLiftedState = { - initialState, - actions: [INIT_ACTION] + committedState: initialState, + stagedActions: [INIT_ACTION] }; /** * Manages how the DevTools actions modify the DevTools state. */ return function liftedReducer(liftedState = initialLiftedState, liftedAction) { + let { committedState, stagedActions, computations } = liftedState; + switch (liftedAction.type) { case ActionTypes.RESET: - liftedState = { - ...liftedState, - actions: [INIT_ACTION], - initialState - }; + stagedActions = [INIT_ACTION]; + committedState = initialState; break; case ActionTypes.COMMIT: - const { computations } = liftedState; - liftedState = { - ...liftedState, - actions: [INIT_ACTION], - initialState: last(computations).state - }; + stagedActions = [INIT_ACTION]; + committedState = last(computations).state; break; case ActionTypes.ROLLBACK: - liftedState = { - ...liftedState, - actions: [INIT_ACTION] - }; + stagedActions = [INIT_ACTION]; break; case ActionTypes.PERFORM_ACTION: - const { actions } = liftedState; - const { action } = liftedAction; - liftedState = { - ...liftedState, - actions: [...actions, action] - }; + stagedActions = [...stagedActions, liftedAction.action]; break; } - return { - ...liftedState, - computations: recompute(reducer, liftedState) - }; + computations = recompute(reducer, committedState, stagedActions); + return { committedState, stagedActions, computations }; }; } From d074733d8588869d50fd8fbc630638202740bd48 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Fri, 10 Jul 2015 22:55:19 +0300 Subject: [PATCH 07/14] Support toggling actions and sweeping --- .../counter/redux-devtools/ReduxMonitor.js | 29 ++++--- .../redux-devtools/ReduxMonitorEntry.js | 20 ++--- examples/counter/redux-devtools/devtools.js | 77 +++++++++++++++---- 3 files changed, 86 insertions(+), 40 deletions(-) diff --git a/examples/counter/redux-devtools/ReduxMonitor.js b/examples/counter/redux-devtools/ReduxMonitor.js index 9fe515d575..2968e7c595 100644 --- a/examples/counter/redux-devtools/ReduxMonitor.js +++ b/examples/counter/redux-devtools/ReduxMonitor.js @@ -7,12 +7,12 @@ import values from 'lodash/object/values'; @connect(state => ({ stagedActions: state.stagedActions, - computations: state.computations, - disabledActions: state.disabledActions || {} // TODO + computedStates: state.computedStates, + skippedActions: state.skippedActions })) export default class ReduxMonitor { static propTypes = { - computations: PropTypes.array.isRequired, + computedStates: PropTypes.array.isRequired, select: PropTypes.func.isRequired }; @@ -21,7 +21,7 @@ export default class ReduxMonitor { }; componentWillReceiveProps(nextProps) { - if (this.props.computations.length < nextProps.computations.length) { + if (this.props.computedStates.length < nextProps.computedStates.length) { const scrollableNode = findDOMNode(this).parentElement; const { scrollTop, offsetHeight, scrollHeight } = scrollableNode; @@ -35,7 +35,7 @@ export default class ReduxMonitor { componentDidUpdate(prevProps) { if ( - prevProps.computations.length < this.props.computations.length && + prevProps.computedStates.length < this.props.computedStates.length && this.scrollDown ) { const scrollableNode = findDOMNode(this).parentElement; @@ -64,11 +64,10 @@ export default class ReduxMonitor { }); } - handleToggleAction(index, toggleMany) { + handleToggleAction(index) { this.props.dispatch({ type: ActionTypes.TOGGLE_ACTION, - index, - toggleMany + index }); } @@ -80,11 +79,11 @@ export default class ReduxMonitor { render() { const elements = []; - const { disabledActions, stagedActions, computations, select } = this.props; + const { skippedActions, stagedActions, computedStates, select } = this.props; for (let i = 0; i < stagedActions.length; i++) { const action = stagedActions[i]; - const { state, error } = computations[i]; + const { state, error } = computedStates[i]; elements.push( ); } @@ -111,13 +110,13 @@ export default class ReduxMonitor {
{elements}
- {computations.length > 1 && + {computedStates.length > 1 && Rollback } - {values(disabledActions).some(identity) && + {values(skippedActions).some(identity) && {' • '} } - {computations.length > 1 && + {computedStates.length > 1 && {' • '} diff --git a/examples/counter/redux-devtools/ReduxMonitorEntry.js b/examples/counter/redux-devtools/ReduxMonitorEntry.js index 00d5459306..eaa530a772 100644 --- a/examples/counter/redux-devtools/ReduxMonitorEntry.js +++ b/examples/counter/redux-devtools/ReduxMonitorEntry.js @@ -39,17 +39,17 @@ export default class ReduxMonitorEntry { state: PropTypes.object.isRequired, action: PropTypes.object.isRequired, select: PropTypes.func.isRequired, - errorText: PropTypes.string, + error: PropTypes.string, onActionClick: PropTypes.func.isRequired, collapsed: PropTypes.bool }; - printState(state, errorText) { - if (!errorText) { + printState(state, error) { + if (!error) { try { return JSON.stringify(this.props.select(state)); } catch (err) { - errorText = 'Error selecting state.'; + error = 'Error selecting state.'; } } @@ -57,18 +57,20 @@ export default class ReduxMonitorEntry { - ({errorText}) + ({error}) ); } handleActionClick(e) { const { index, onActionClick } = this.props; - onActionClick(index, e.ctrlKey || e.metaKey); + if (index > 0) { + onActionClick(index); + } } render() { - const { index, errorText, action, state, collapsed, onActionClick } = this.props; + const { index, error, action, state, collapsed, onActionClick } = this.props; const { type = '' } = action; const { r, g, b } = colorFromString(action.type); @@ -84,7 +86,7 @@ export default class ReduxMonitorEntry { paddingBottom: '1em', paddingTop: '1em', color: `rgb(${r}, ${g}, ${b})`, - cursor: 'hand', + cursor: (index > 0) ? 'hand' : 'default', WebkitUserSelect: 'none' }}> {JSON.stringify(action)} @@ -105,7 +107,7 @@ export default class ReduxMonitorEntry { paddingTop: '1em', color: 'lightyellow' }}> - {this.printState(state, errorText)} + {this.printState(state, error)}
} diff --git a/examples/counter/redux-devtools/devtools.js b/examples/counter/redux-devtools/devtools.js index 9a170b28da..08cba15b9f 100644 --- a/examples/counter/redux-devtools/devtools.js +++ b/examples/counter/redux-devtools/devtools.js @@ -2,7 +2,9 @@ export const ActionTypes = { PERFORM_ACTION: 'PERFORM_ACTION', RESET: 'RESET', ROLLBACK: 'ROLLBACK', - COMMIT: 'COMMIT' + COMMIT: 'COMMIT', + TOGGLE_ACTION: 'TOGGLE_ACTION', + SWEEP: 'SWEEP' }; const INIT_ACTION = { @@ -13,6 +15,16 @@ function last(arr) { return arr[arr.length - 1]; } +function toggle(obj, key) { + obj = { ...obj }; + if (obj[key]) { + delete obj[key]; + } else { + obj[key] = true; + } + return obj; +} + /** * Computes the next entry in the log by applying an action. */ @@ -38,21 +50,25 @@ function computeNextEntry(reducer, action, state, error) { * It's probably a good idea to do this only if the code has changed, * but until we have some tests we'll just do it every time an action fires. */ -function recompute(reducer, committedState, stagedActions) { - const computations = []; +function recomputeStates(reducer, committedState, stagedActions, skippedActions) { + const computedStates = []; for (let i = 0; i < stagedActions.length; i++) { const action = stagedActions[i]; - const previousEntry = computations[i - 1]; + const previousEntry = computedStates[i - 1]; const previousState = previousEntry ? previousEntry.state : committedState; const previousError = previousEntry ? previousEntry.error : undefined; - const entry = computeNextEntry(reducer, action, previousState, previousError); - computations.push(entry); + const shouldSkip = Boolean(skippedActions[i]); + const entry = shouldSkip ? + previousEntry : + computeNextEntry(reducer, action, previousState, previousError); + + computedStates.push(entry); } - return computations; + return computedStates; } @@ -62,34 +78,63 @@ function recompute(reducer, committedState, stagedActions) { function liftReducer(reducer, initialState) { const initialLiftedState = { committedState: initialState, - stagedActions: [INIT_ACTION] + stagedActions: [INIT_ACTION], + skippedActions: {} }; /** * Manages how the DevTools actions modify the DevTools state. */ return function liftedReducer(liftedState = initialLiftedState, liftedAction) { - let { committedState, stagedActions, computations } = liftedState; + let { + committedState, + stagedActions, + skippedActions, + computedStates + } = liftedState; switch (liftedAction.type) { case ActionTypes.RESET: - stagedActions = [INIT_ACTION]; committedState = initialState; + stagedActions = [INIT_ACTION]; + skippedActions = {}; break; case ActionTypes.COMMIT: + committedState = last(computedStates).state; stagedActions = [INIT_ACTION]; - committedState = last(computations).state; + skippedActions = {}; break; case ActionTypes.ROLLBACK: stagedActions = [INIT_ACTION]; + skippedActions = {}; + break; + case ActionTypes.TOGGLE_ACTION: + const { index } = liftedAction; + skippedActions = toggle(skippedActions, index); + break; + case ActionTypes.SWEEP: + stagedActions = stagedActions.filter((_, i) => !skippedActions[i]); + skippedActions = {}; break; case ActionTypes.PERFORM_ACTION: - stagedActions = [...stagedActions, liftedAction.action]; + const { action } = liftedAction; + stagedActions = [...stagedActions, action]; break; } - computations = recompute(reducer, committedState, stagedActions); - return { committedState, stagedActions, computations }; + computedStates = recomputeStates( + reducer, + committedState, + stagedActions, + skippedActions + ); + + return { + committedState, + stagedActions, + skippedActions, + computedStates + }; }; } @@ -105,8 +150,8 @@ function liftAction(action) { * Unlifts the DevTools state to the app state. */ function unliftState(liftedState) { - const { computations } = liftedState; - const { state } = last(computations); + const { computedStates } = liftedState; + const { state } = last(computedStates); return state; } From 084b19483c3ca2611ba5ce0d83ab9f4b38383109 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Fri, 10 Jul 2015 23:39:26 +0300 Subject: [PATCH 08/14] Make ReduxMonitor independent of Redux --- examples/counter/containers/App.js | 16 ++- .../counter/redux-devtools/ReduxMonitor.js | 44 +++---- examples/counter/redux-devtools/TODO | 3 - examples/counter/redux-devtools/devtools.js | 27 +++- .../counter/redux-devtools/devtools_old.js | 115 ------------------ 5 files changed, 52 insertions(+), 153 deletions(-) delete mode 100644 examples/counter/redux-devtools/TODO delete mode 100644 examples/counter/redux-devtools/devtools_old.js diff --git a/examples/counter/containers/App.js b/examples/counter/containers/App.js index 0139a271fb..e3ce0e2172 100644 --- a/examples/counter/containers/App.js +++ b/examples/counter/containers/App.js @@ -1,11 +1,11 @@ import React, { Component } from 'react'; import CounterApp from './CounterApp'; -import { createStore, applyMiddleware, compose, combineReducers } from 'redux'; -import { Provider } from 'redux/react'; +import { createStore, applyMiddleware, compose, combineReducers, bindActionCreators } from 'redux'; +import { Provider, Connector } from 'redux/react'; import * as reducers from '../reducers'; import DebugPanel from '../redux-devtools/DebugPanel'; -import devtools from '../redux-devtools/devtools'; +import devtools, { ActionCreators } from '../redux-devtools/devtools'; import ReduxMonitor from '../redux-devtools/ReduxMonitor'; // TODO: move into a separate project @@ -36,7 +36,15 @@ export default class App extends Component { - {() => } + {() => + + {({ dispatch, ...props }) => + + } + + } diff --git a/examples/counter/redux-devtools/ReduxMonitor.js b/examples/counter/redux-devtools/ReduxMonitor.js index 2968e7c595..d479c0ad28 100644 --- a/examples/counter/redux-devtools/ReduxMonitor.js +++ b/examples/counter/redux-devtools/ReduxMonitor.js @@ -1,27 +1,26 @@ import React, { PropTypes, findDOMNode } from 'react'; import { ActionTypes } from './devtools'; -import { connect } from '../../../src/react'; import ReduxMonitorEntry from './ReduxMonitorEntry'; -import identity from 'lodash/utility/identity'; -import values from 'lodash/object/values'; -@connect(state => ({ - stagedActions: state.stagedActions, - computedStates: state.computedStates, - skippedActions: state.skippedActions -})) export default class ReduxMonitor { static propTypes = { computedStates: PropTypes.array.isRequired, + stagedActions: PropTypes.array.isRequired, + skippedActions: PropTypes.object.isRequired, + reset: PropTypes.func.isRequired, + commit: PropTypes.func.isRequired, + rollback: PropTypes.func.isRequired, + sweep: PropTypes.func.isRequired, + toggleAction: PropTypes.func.isRequired, select: PropTypes.func.isRequired }; static defaultProps = { - select: identity + select: (state) => state }; componentWillReceiveProps(nextProps) { - if (this.props.computedStates.length < nextProps.computedStates.length) { + if (this.props.stagedActions.length < nextProps.stagedActions.length) { const scrollableNode = findDOMNode(this).parentElement; const { scrollTop, offsetHeight, scrollHeight } = scrollableNode; @@ -35,7 +34,7 @@ export default class ReduxMonitor { componentDidUpdate(prevProps) { if ( - prevProps.computedStates.length < this.props.computedStates.length && + prevProps.stagedActions.length < this.props.stagedActions.length && this.scrollDown ) { const scrollableNode = findDOMNode(this).parentElement; @@ -47,34 +46,23 @@ export default class ReduxMonitor { } handleRollback() { - this.props.dispatch({ - type: ActionTypes.ROLLBACK - }); + this.props.rollback(); } handleSweep() { - this.props.dispatch({ - type: ActionTypes.SWEEP - }); + this.props.sweep(); } handleCommit() { - this.props.dispatch({ - type: ActionTypes.COMMIT - }); + this.props.commit(); } handleToggleAction(index) { - this.props.dispatch({ - type: ActionTypes.TOGGLE_ACTION, - index - }); + this.props.toggleAction(index); } handleReset() { - this.props.dispatch({ - type: ActionTypes.RESET - }); + this.props.reset(); } render() { @@ -116,7 +104,7 @@ export default class ReduxMonitor { Rollback } - {values(skippedActions).some(identity) && + {Object.keys(skippedActions).some(key => skippedActions[key]) && {' • '} !disabledActions[i]); - atoms = atoms.filter((_, i) => !disabledActions[i]); - disabledActions = {}; - break; - case ActionTypes.TOGGLE_ACTION: - if (action.toggleMany) { - disabledActions = {}; - for (let i = 0; i < actions.length; i++) { - disabledActions[i] = i > action.index; - } - } else { - disabledActions = { - ...disabledActions, - [action.index]: !disabledActions[action.index] - }; - } - break; - } - - error = null; - actions = [...actions, action]; - atoms = actions.reduce((allAtoms, nextAction) => { - const index = allAtoms.length; - const prevAtom = index > 0 ? - allAtoms[index - 1] : - initialAtom; - - if (error || disabledActions[index] === true) { - allAtoms.push(prevAtom); - } else { - try { - allAtoms.push(store(prevAtom, nextAction)); - } catch (err) { - error = { - index: index, - text: err.toString() - }; - console.error(err); - allAtoms.push(prevAtom); - } - } - - return allAtoms; - }, []); - const nextAtom = atoms[atoms.length - 1]; - - switch (action.type) { - case ActionTypes.INIT: - if (atoms.length > 1) { - atoms.pop(); - actions.pop(); - } - break; - case ActionTypes.TOGGLE_ACTION: - case ActionTypes.SWEEP: - atoms.pop(); - actions.pop(); - break; - } - - return { - [StateKeys.ACTIONS]: actions, - [StateKeys.DISABLED_ACTIONS]: disabledActions, - [StateKeys.STATES]: atoms, - [StateKeys.INITIAL_STATE]: initialAtom, - [StateKeys.ERROR]: error, - ...nextAtom - }; - }; -} From 6b87465923acb53201f8e2567083439a2b1d504b Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Sat, 11 Jul 2015 00:15:23 +0300 Subject: [PATCH 09/14] Add simpler user-faced API --- examples/counter/containers/App.js | 17 +- .../{ReduxMonitorEntry.js => Entry.js} | 3 +- examples/counter/redux-devtools/Monitor.js | 130 +++++++++++++++ .../counter/redux-devtools/ReduxMonitor.js | 150 ++++-------------- .../redux-devtools/{devtools.js => index.js} | 6 +- 5 files changed, 166 insertions(+), 140 deletions(-) rename examples/counter/redux-devtools/{ReduxMonitorEntry.js => Entry.js} (98%) create mode 100644 examples/counter/redux-devtools/Monitor.js rename examples/counter/redux-devtools/{devtools.js => index.js} (97%) diff --git a/examples/counter/containers/App.js b/examples/counter/containers/App.js index e3ce0e2172..8723d5be38 100644 --- a/examples/counter/containers/App.js +++ b/examples/counter/containers/App.js @@ -4,8 +4,8 @@ import { createStore, applyMiddleware, compose, combineReducers, bindActionCreat import { Provider, Connector } from 'redux/react'; import * as reducers from '../reducers'; +import devTools from '../redux-devtools/index'; import DebugPanel from '../redux-devtools/DebugPanel'; -import devtools, { ActionCreators } from '../redux-devtools/devtools'; import ReduxMonitor from '../redux-devtools/ReduxMonitor'; // TODO: move into a separate project @@ -18,13 +18,12 @@ function thunk({ dispatch, getState }) { const finalCreateStore = compose( applyMiddleware(), - devtools(), + devTools(), createStore ); const reducer = combineReducers(reducers); const store = finalCreateStore(combineReducers(reducers)); -const devToolsStore = store.getDevToolsStore(); export default class App extends Component { render() { @@ -35,17 +34,7 @@ export default class App extends Component { - - {() => - - {({ dispatch, ...props }) => - - } - - } - + ); diff --git a/examples/counter/redux-devtools/ReduxMonitorEntry.js b/examples/counter/redux-devtools/Entry.js similarity index 98% rename from examples/counter/redux-devtools/ReduxMonitorEntry.js rename to examples/counter/redux-devtools/Entry.js index eaa530a772..6447b768f8 100644 --- a/examples/counter/redux-devtools/ReduxMonitorEntry.js +++ b/examples/counter/redux-devtools/Entry.js @@ -32,8 +32,7 @@ function colorFromString(token) { return hsvToRgb(h, s, v); } - -export default class ReduxMonitorEntry { +export default class Entry { static propTypes = { index: PropTypes.number.isRequired, state: PropTypes.object.isRequired, diff --git a/examples/counter/redux-devtools/Monitor.js b/examples/counter/redux-devtools/Monitor.js new file mode 100644 index 0000000000..baddec54ac --- /dev/null +++ b/examples/counter/redux-devtools/Monitor.js @@ -0,0 +1,130 @@ +import React, { PropTypes, findDOMNode } from 'react'; +import Entry from './Entry'; + +export default class Monitor { + static propTypes = { + computedStates: PropTypes.array.isRequired, + stagedActions: PropTypes.array.isRequired, + skippedActions: PropTypes.object.isRequired, + reset: PropTypes.func.isRequired, + commit: PropTypes.func.isRequired, + rollback: PropTypes.func.isRequired, + sweep: PropTypes.func.isRequired, + toggleAction: PropTypes.func.isRequired, + select: PropTypes.func.isRequired + }; + + static defaultProps = { + select: (state) => state + }; + + componentWillReceiveProps(nextProps) { + if (this.props.stagedActions.length < nextProps.stagedActions.length) { + const scrollableNode = findDOMNode(this).parentElement; + const { scrollTop, offsetHeight, scrollHeight } = scrollableNode; + + this.scrollDown = Math.abs( + scrollHeight - (scrollTop + offsetHeight) + ) < 20; + } else { + this.scrollDown = false; + } + } + + componentDidUpdate(prevProps) { + if ( + prevProps.stagedActions.length < this.props.stagedActions.length && + this.scrollDown + ) { + const scrollableNode = findDOMNode(this).parentElement; + const { scrollTop, offsetHeight, scrollHeight } = scrollableNode; + + scrollableNode.scrollTop = scrollHeight - offsetHeight; + this.scrollDown = false; + } + } + + handleRollback() { + this.props.rollback(); + } + + handleSweep() { + this.props.sweep(); + } + + handleCommit() { + this.props.commit(); + } + + handleToggleAction(index) { + this.props.toggleAction(index); + } + + handleReset() { + this.props.reset(); + } + + render() { + const elements = []; + const { skippedActions, stagedActions, computedStates, select } = this.props; + + for (let i = 0; i < stagedActions.length; i++) { + const action = stagedActions[i]; + const { state, error } = computedStates[i]; + + elements.push( + + ); + } + + return ( +
+ + {elements} +
+ {computedStates.length > 1 && + + Rollback + + } + {Object.keys(skippedActions).some(key => skippedActions[key]) && + + {' • '} + + Sweep + + + } + {computedStates.length > 1 && + + + {' • '} + + + Commit + + + } +
+
+ ); + } +} diff --git a/examples/counter/redux-devtools/ReduxMonitor.js b/examples/counter/redux-devtools/ReduxMonitor.js index d479c0ad28..5a3b585d31 100644 --- a/examples/counter/redux-devtools/ReduxMonitor.js +++ b/examples/counter/redux-devtools/ReduxMonitor.js @@ -1,131 +1,41 @@ -import React, { PropTypes, findDOMNode } from 'react'; -import { ActionTypes } from './devtools'; -import ReduxMonitorEntry from './ReduxMonitorEntry'; +import React, { PropTypes } from 'react'; +import { bindActionCreators } from 'redux'; +import { Provider, Connector } from 'redux/react'; +import { ActionCreators } from './index'; +import Monitor from './Monitor'; export default class ReduxMonitor { static propTypes = { - computedStates: PropTypes.array.isRequired, - stagedActions: PropTypes.array.isRequired, - skippedActions: PropTypes.object.isRequired, - reset: PropTypes.func.isRequired, - commit: PropTypes.func.isRequired, - rollback: PropTypes.func.isRequired, - sweep: PropTypes.func.isRequired, - toggleAction: PropTypes.func.isRequired, - select: PropTypes.func.isRequired + store: PropTypes.shape({ + devToolsStore: PropTypes.shape({ + dispatch: PropTypes.func.isRequired + }).isRequired + }).isRequired, + select: PropTypes.func }; - static defaultProps = { - select: (state) => state - }; - - componentWillReceiveProps(nextProps) { - if (this.props.stagedActions.length < nextProps.stagedActions.length) { - const scrollableNode = findDOMNode(this).parentElement; - const { scrollTop, offsetHeight, scrollHeight } = scrollableNode; - - this.scrollDown = Math.abs( - scrollHeight - (scrollTop + offsetHeight) - ) < 20; - } else { - this.scrollDown = false; - } - } - - componentDidUpdate(prevProps) { - if ( - prevProps.stagedActions.length < this.props.stagedActions.length && - this.scrollDown - ) { - const scrollableNode = findDOMNode(this).parentElement; - const { scrollTop, offsetHeight, scrollHeight } = scrollableNode; - - scrollableNode.scrollTop = scrollHeight - offsetHeight; - this.scrollDown = false; - } - } - - handleRollback() { - this.props.rollback(); - } - - handleSweep() { - this.props.sweep(); - } - - handleCommit() { - this.props.commit(); - } - - handleToggleAction(index) { - this.props.toggleAction(index); - } - - handleReset() { - this.props.reset(); - } - render() { - const elements = []; - const { skippedActions, stagedActions, computedStates, select } = this.props; - - for (let i = 0; i < stagedActions.length; i++) { - const action = stagedActions[i]; - const { state, error } = computedStates[i]; + const { devToolsStore } = this.props.store; + return ( + + {this.renderRoot} + + ); + } - elements.push( - - ); - } + renderRoot = () => { + return ( + + {this.renderMonitor} + + ); + }; + renderMonitor = ({ dispatch, ...props }) => { return ( -
- - {elements} -
- {computedStates.length > 1 && - - Rollback - - } - {Object.keys(skippedActions).some(key => skippedActions[key]) && - - {' • '} - - Sweep - - - } - {computedStates.length > 1 && - - - {' • '} - - - Commit - - - } -
-
+ ); - } + }; } diff --git a/examples/counter/redux-devtools/devtools.js b/examples/counter/redux-devtools/index.js similarity index 97% rename from examples/counter/redux-devtools/devtools.js rename to examples/counter/redux-devtools/index.js index 1743e6a715..c13c20de73 100644 --- a/examples/counter/redux-devtools/devtools.js +++ b/examples/counter/redux-devtools/index.js @@ -167,15 +167,13 @@ function unliftStore(liftedStore) { getState() { return unliftState(liftedStore.getState()); }, - getDevToolsStore() { - return liftedStore; - } + devToolsStore: liftedStore }; return store; } /** - * Action creators to manage DevTools state. + * Action creators to change the DevTools state. */ export const ActionCreators = { reset() { From d2e2531b86d3591063005042e3a76e2aded7c25b Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Sat, 11 Jul 2015 00:19:05 +0300 Subject: [PATCH 10/14] Remove some unused stuff --- examples/counter/containers/App.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/counter/containers/App.js b/examples/counter/containers/App.js index 8723d5be38..c2645849b9 100644 --- a/examples/counter/containers/App.js +++ b/examples/counter/containers/App.js @@ -1,7 +1,7 @@ import React, { Component } from 'react'; import CounterApp from './CounterApp'; -import { createStore, applyMiddleware, compose, combineReducers, bindActionCreators } from 'redux'; -import { Provider, Connector } from 'redux/react'; +import { createStore, applyMiddleware, compose, combineReducers } from 'redux'; +import { Provider } from 'redux/react'; import * as reducers from '../reducers'; import devTools from '../redux-devtools/index'; From 3d357f55d02e6279333745b49e68bf704a76b9d8 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Sat, 11 Jul 2015 02:20:53 +0300 Subject: [PATCH 11/14] Use prototypal inheritance --- .babelrc | 3 ++- examples/counter/redux-devtools/index.js | 13 +++++++------ package.json | 1 + 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/.babelrc b/.babelrc index 15d27ad9b4..166ed4e2d6 100644 --- a/.babelrc +++ b/.babelrc @@ -1,4 +1,5 @@ { "stage": 0, - "loose": "all" + "loose": "all", + "plugins": ["object-assign"] } diff --git a/examples/counter/redux-devtools/index.js b/examples/counter/redux-devtools/index.js index c13c20de73..dc24263c07 100644 --- a/examples/counter/redux-devtools/index.js +++ b/examples/counter/redux-devtools/index.js @@ -159,17 +159,18 @@ function unliftState(liftedState) { * Unlifts the DevTools store to act like the app's store. */ function unliftStore(liftedStore) { - const store = { - ...liftedStore, + return Object.assign(Object.create(liftedStore), { + devToolsStore: liftedStore, + dispatch(action) { liftedStore.dispatch(liftAction(action)); + return action; }, + getState() { return unliftState(liftedStore.getState()); - }, - devToolsStore: liftedStore - }; - return store; + } + }); } /** diff --git a/package.json b/package.json index 4ba22084a1..20ed416664 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "babel-core": "5.6.15", "babel-eslint": "^3.1.15", "babel-loader": "^5.1.4", + "babel-plugin-object-assign": "^1.2.0", "eslint": "^0.23", "eslint-config-airbnb": "0.0.6", "eslint-plugin-react": "^2.3.0", From 35737c5ef8851b24034c0cc538a8ca253ae9b77b Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Sat, 11 Jul 2015 02:52:47 +0300 Subject: [PATCH 12/14] Add debug session persistance --- examples/counter/containers/App.js | 2 ++ .../counter/redux-devtools/persistState.js | 30 +++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 examples/counter/redux-devtools/persistState.js diff --git a/examples/counter/containers/App.js b/examples/counter/containers/App.js index c2645849b9..1231950e56 100644 --- a/examples/counter/containers/App.js +++ b/examples/counter/containers/App.js @@ -5,6 +5,7 @@ import { Provider } from 'redux/react'; import * as reducers from '../reducers'; import devTools from '../redux-devtools/index'; +import persistState from '../redux-devtools/persistState'; import DebugPanel from '../redux-devtools/DebugPanel'; import ReduxMonitor from '../redux-devtools/ReduxMonitor'; @@ -19,6 +20,7 @@ function thunk({ dispatch, getState }) { const finalCreateStore = compose( applyMiddleware(), devTools(), + persistState(window.location.href.match(/[?&]debug_session=([^&]+)\b/)), createStore ); diff --git a/examples/counter/redux-devtools/persistState.js b/examples/counter/redux-devtools/persistState.js new file mode 100644 index 0000000000..5a438b319f --- /dev/null +++ b/examples/counter/redux-devtools/persistState.js @@ -0,0 +1,30 @@ +export default function persistState(sessionId) { + if (!sessionId) { + return next => (...args) => next(...args); + } + + return next => (reducer, initialState) => { + const key = `redux-dev-session-${sessionId}`; + + try { + initialState = JSON.parse(localStorage.getItem(key)) || initialState; + next(reducer, initialState); + } catch (e) { + try { + localStorage.removeItem(key); + } finally { + initialState = undefined; + } + } + + const store = next(reducer, initialState); + + return Object.assign(Object.create(store), { + dispatch(action) { + store.dispatch(action); + localStorage.setItem(key, JSON.stringify(store.getState())); + return action; + } + }); + }; +} From e7f8a8469f0a21bf7e55db7b20eb0d69c656bb4f Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 13 Jul 2015 03:41:03 +0300 Subject: [PATCH 13/14] Fixes --- examples/counter/containers/App.js | 2 +- examples/counter/redux-devtools/ReduxMonitor.js | 2 +- examples/counter/redux-devtools/index.js | 7 +++---- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/examples/counter/containers/App.js b/examples/counter/containers/App.js index 1231950e56..ae75692994 100644 --- a/examples/counter/containers/App.js +++ b/examples/counter/containers/App.js @@ -1,7 +1,7 @@ import React, { Component } from 'react'; import CounterApp from './CounterApp'; import { createStore, applyMiddleware, compose, combineReducers } from 'redux'; -import { Provider } from 'redux/react'; +import { Provider } from 'react-redux'; import * as reducers from '../reducers'; import devTools from '../redux-devtools/index'; diff --git a/examples/counter/redux-devtools/ReduxMonitor.js b/examples/counter/redux-devtools/ReduxMonitor.js index 5a3b585d31..46ede1e468 100644 --- a/examples/counter/redux-devtools/ReduxMonitor.js +++ b/examples/counter/redux-devtools/ReduxMonitor.js @@ -1,6 +1,6 @@ import React, { PropTypes } from 'react'; import { bindActionCreators } from 'redux'; -import { Provider, Connector } from 'redux/react'; +import { Provider, Connector } from 'react-redux'; import { ActionCreators } from './index'; import Monitor from './Monitor'; diff --git a/examples/counter/redux-devtools/index.js b/examples/counter/redux-devtools/index.js index dc24263c07..62392ab5eb 100644 --- a/examples/counter/redux-devtools/index.js +++ b/examples/counter/redux-devtools/index.js @@ -159,18 +159,17 @@ function unliftState(liftedState) { * Unlifts the DevTools store to act like the app's store. */ function unliftStore(liftedStore) { - return Object.assign(Object.create(liftedStore), { + return { + ...liftedStore, devToolsStore: liftedStore, - dispatch(action) { liftedStore.dispatch(liftAction(action)); return action; }, - getState() { return unliftState(liftedStore.getState()); } - }); + }; } /** From e065a7fbcccaeb0320d9237f38f7d492995405ca Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 13 Jul 2015 03:45:03 +0300 Subject: [PATCH 14/14] Tweak dependencies --- examples/counter/package.json | 2 +- examples/counter/webpack.config.js | 10 +++++++--- examples/todomvc/package.json | 2 +- examples/todomvc/webpack.config.js | 13 +++++++++---- package.json | 4 +--- 5 files changed, 19 insertions(+), 12 deletions(-) diff --git a/examples/counter/package.json b/examples/counter/package.json index 9206cb430a..72c81ee4f7 100644 --- a/examples/counter/package.json +++ b/examples/counter/package.json @@ -32,7 +32,7 @@ "react-redux": "^1.0.0-alpha" }, "devDependencies": { - "babel-core": "^5.5.8", + "babel-core": "^5.6.18", "babel-loader": "^5.1.4", "node-libs-browser": "^0.5.2", "react-hot-loader": "^1.2.7", diff --git a/examples/counter/webpack.config.js b/examples/counter/webpack.config.js index 2e86f7e7ea..2061e19a48 100644 --- a/examples/counter/webpack.config.js +++ b/examples/counter/webpack.config.js @@ -19,8 +19,7 @@ module.exports = { ], resolve: { alias: { - 'redux': path.join(__dirname, '..', '..', 'src'), - 'react': path.join(__dirname, '..', '..', 'node_modules', 'react') + 'redux': path.join(__dirname, '..', '..', 'src') }, extensions: ['', '.js'] }, @@ -28,7 +27,12 @@ module.exports = { loaders: [{ test: /\.js$/, loaders: ['react-hot', 'babel'], - exclude: /node_modules/ + exclude: /node_modules/, + include: __dirname + }, { + test: /\.js$/, + loaders: ['babel'], + include: path.join(__dirname, '..', '..', 'src') }] } }; diff --git a/examples/todomvc/package.json b/examples/todomvc/package.json index 4e50e51038..0e0293f7ff 100644 --- a/examples/todomvc/package.json +++ b/examples/todomvc/package.json @@ -34,7 +34,7 @@ "react-redux": "^1.0.0-alpha" }, "devDependencies": { - "babel-core": "^5.5.8", + "babel-core": "^5.6.18", "babel-loader": "^5.1.4", "node-libs-browser": "^0.5.2", "raw-loader": "^0.5.1", diff --git a/examples/todomvc/webpack.config.js b/examples/todomvc/webpack.config.js index 9a949855bd..6b487b38c6 100644 --- a/examples/todomvc/webpack.config.js +++ b/examples/todomvc/webpack.config.js @@ -19,8 +19,7 @@ module.exports = { ], resolve: { alias: { - 'redux': path.join(__dirname, '..', '..', 'src'), - 'react': path.join(__dirname, '..', '..', 'node_modules', 'react') + 'redux': path.join(__dirname, '..', '..', 'src') }, extensions: ['', '.js'] }, @@ -28,10 +27,16 @@ module.exports = { loaders: [{ test: /\.js$/, loaders: ['react-hot', 'babel'], - exclude: /node_modules/ + exclude: /node_modules/, + include: __dirname + }, { + test: /\.js$/, + loaders: ['babel'], + include: path.join(__dirname, '..', '..', 'src') }, { test: /\.css?$/, - loaders: ['style', 'raw'] + loaders: ['style', 'raw'], + include: __dirname }] } }; diff --git a/package.json b/package.json index 20ed416664..751d38edbd 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "homepage": "https://github.com/gaearon/redux", "devDependencies": { "babel": "^5.5.8", - "babel-core": "5.6.15", + "babel-core": "^5.6.18", "babel-eslint": "^3.1.15", "babel-loader": "^5.1.4", "babel-plugin-object-assign": "^1.2.0", @@ -46,8 +46,6 @@ "expect": "^1.6.0", "isparta": "^3.0.3", "mocha": "^2.2.5", - "react": "^0.13.0", - "react-hot-loader": "^1.2.7", "rimraf": "^2.3.4", "webpack": "^1.9.6", "webpack-dev-server": "^1.8.2"