Techniques for using Redux & React specifically for people building large applications
Sunjay Varma
const React = require('react');
const Collapsible = React.createClass({
getInitialState() {
return {
collapsed: false,
};
},
toggleCollapsed() {
this.setState({
collapsed: !this.state.collapsed,
});
},
render() {
const collapsed = this.state.collapsed;
return (
<div>
<button onClick={this.toggleCollapsed}>
{collapsed ? 'Uncollapse' : 'Collapse'}
</button>
<div>
{!collapsed ?
this.props.children
: null
}
</div>
</div>
);
},
});
const React = require('react');
const Collapsible = ({collapsed, onToggleCollapsed, children}) => (
<div>
<button onClick={onToggleCollapsed}>
{collapsed ? 'Uncollapse' : 'Collapse'}
</button>
<div>
{!collapsed ? children : null}
</div>
</div>
);
Collapsible.propTypes = {
collapsed: React.PropTypes.bool.isRequired,
onToggleCollapsed: React.PropTypes.func,
children: React.PropTypes.node,
};
From the Redux documentation:
"Redux is a predictable state container for JavaScript apps"
Model
View
Controller
Unidirectional Data Flow
A single pipe for all your actions to go through
As many of these as you want
Not Allowed
Image from: http://facebook.github.io/flux/docs/overview.html#structure-and-data-flow
Single Source of Truth
The state of your whole application is stored in an object tree within a single store.
State is read-only
The only way to mutate the state is to emit an action, an object describing what happened.
Changes are made with pure functions
To specify how the state tree is transformed by actions, you write pure reducers.
From: http://redux.js.org/docs/introduction/ThreePrinciples.html
Same Unidirectional Data Flow
Image from: http://facebook.github.io/flux/docs/overview.html#structure-and-data-flow
Reducer
Redux passes actions through the reducer and then sends the updated state to the view
Reducer can be split into many reducers
only one of these
Single Source of Truth
The state of your whole application is stored in an object tree within a single store.
State is read-only
The only way to mutate the state is to emit an action, an object describing what happened.
Changes are made with pure functions
To specify how the state tree is transformed by actions, you write pure reducers.
From: http://redux.js.org/docs/introduction/ThreePrinciples.html
Single Source of Truth
The state of your whole application is stored in an object tree within a single store.
State is read-only
The only way to mutate the state is to emit an action, an object describing what happened.
Changes are made with pure functions
To specify how the state tree is transformed by actions, you write pure reducers.
From: http://redux.js.org/docs/introduction/ThreePrinciples.html
Single Source of Truth
The state of your whole application is stored in an object tree within a single store.
State is read-only
The only way to mutate the state is to emit an action, an object describing what happened.
Changes are made with pure functions
To specify how the state tree is transformed by actions, you write pure reducers.
From: http://redux.js.org/docs/introduction/ThreePrinciples.html
Bringing two worlds together
const React = require('react');
const Toolbar = ({
selected, onCreate, onDelete
}) => (
<div className='action-bar'>
<span>{selected} Selected</span>
<button onClick={onCreate}>Create</button>
<button onClick={onDelete}>Delete</button>
</div>
);
const {connect} = require('react-redux');
const mapStateToProps = ({selected}) => ({
selected,
});
const mapDispatchToProps = (dispatch) => ({
onCreate() {
dispatch({type: 'CREATE'});
},
onDelete() {
dispatch({type: 'DELETE'});
},
});
const ActionBar = connect(
mapStateToProps,
mapDispatchToProps
)(Toolbar);
ReactDOM.render(
<Provider store={store}>
<Router>
...
</Router>
</Provider>,
document.getElementById('body-container')
);
Reducer
Action
Store
Network Request
View
New Store State
Container
Component
Action
New Store State
New Store State
mapStateToProps
mapDispatchToProps
Props
Action
The Cycle Continues...
Browser Renderer
Something happens
Redux
React
Techniques/Approaches for solving common problems with React + Redux in a large application
Note: none of these are the "blessed" approaches to these problems. These are real examples of things I've tried in code that seem to work well.
When you're building a single page app that's more than one page
const createRouter = (history) => (
<Router history={history}>
<Route path='/' component={App}>
<IndexRedirect to='/things' />
<Route path='login' component={Login} />
<Route path='things' component={Things}>
<IndexRoute component={Foo} />
<Route path='stuff/:stuffId' component={Stuff} />
</Route>
<Route path='settings' component={Settings} />
<Route path='*' component={NotFound}/>
</Route>
</Router>
);
// Requires its reducer to be part of the store
const store = createStore(
combineReducers({
...reducers,
routing: routerReducer,
})
);
// Create an enhanced history that syncs navigation events with the store
const history = syncHistoryWithStore(browserHistory, store);
// Use the enhanced history in the router
ReactDOM.render(
<Provider store={store}>
{createRouter(history)}
</Provider>,
document.getElementById('body-container')
);
const pageReducers = [
{pattern: /^\/things\/?.*/, reducer: thingsPage},
{pattern: /^\/login\/?$/, reducer: loginPage},
{pattern: /^\/settings\/?$/, reducer: settingsPage},
];
const page = (topState, action) => {
// Need to reset every time the page changes
if (action.type === LOCATION_CHANGE) {
topState = {
...topState,
page: undefined,
};
}
const location = topState.routing.locationBeforeTransitions;
const pathname = (location || {}).pathname;
return pageReducers.reduce((state, {pattern, reducer}) => {
// Run any matching reducers
if (pattern.test(pathname)) {
return {
...state,
page: reducer(state.page || undefined, action),
};
}
return state;
}, topState);
};
const loginPage = createReducer({
loading: false,
error: null,
nextPathname: '/',
}, {
[ACTION_LOGIN_REQUIRED](state, action) {
return {
...state,
nextPathname: action.nextPathname,
};
},
[ACTION_LOGIN](state) {
return {
...state,
loading: true,
};
},
[ACTION_LOGIN_FAILURE](state, action) {
return {
...state,
loading: false,
error: action.error,
};
},
});
Taming asynchronicity and Race Conditions with Redux
time
first request
(fairly expensive)
second request
(faster)
Outdated first request gets displayed because it comes back last
Comes back first
Race Condition
export const sendMail = (to, body) => {
return sendRequest({
// used to group requests that can supercede each other
id: 'send-mail',
begin() {
return {type: 'SEND_MAIL', to: to, body: body};
},
makeRequest() {
return mailApi.send(to, body);
},
success(response) {
return {type: 'MAIL_SUCCESS'};
},
failure(error) {
return {type: 'MAIL_FAILURE'};
},
});
};
Questions?
Sunjay Varma