Large Applications with Redux & React
Techniques for using Redux & React specifically for people building large applications
Sunjay Varma
Outline
- React (quick overview)
- Redux
- Combing React + Redux
- Route/page specific data
- Request management & race conditions
React
- Used to build components representing the "view" of your application
- JSX is optional but highly recommended
- React components can be stateful or stateless (pure functions)
- Pure function = output only dependent on input with no side effects (always the same output for any given input)
- state = internal to component, props = passed in from outside
React Stateful Component
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>
);
},
});
React Pure Component
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"
- Easy to reason about
- Consistent
- Just the data and how to update the data
- Nothing to do with how the data looks (the view)
Redux is just for your data
- Redux only manages your data as "state" and manages the flow within which you update it
- Redux says nothing about what your data is or how it actually gets updated
- Redux is tiny (~2kB) - 5 functions & 1 class
Typical MVC
Model
View
Controller
The Flux Design Pattern
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
Redux vs. Flux
- Redux takes the best part of flux: the unidirectional data flow
- Makes it even simpler and more predictable
- Redux has three principles
Three Principles of Redux
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
Redux Architecture
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
Three Principles of Redux
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 - Why?
- Makes debugging easy - a single place to look for all the data
- Saving/loading entire state
- Data from the server only needs to exist in a single place
- No need to keep track of where else in the application things are being stored
Three Principles of Redux
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
State is read-only - Why?
- Views or network requests can never modify the state directly
- All mutations are centralized and happen one at a time
- Actions declaratively describe what happened
- Actions are plain objects that can be serialized, logged, stored and even replayed for debugging purposes
- This gets even better with Immutable.js
Why use Immutable.js in your store?
- Typically requires deep comparison
- _.isEqual(a, b) is very expensive
- Immutable.js returns a new object every time you make an update
- Shallow comparison by reference (very fast) becomes viable
- _.isEqual(a, b) vs. a === b
- React PureRenderMixin takes advantage of this for really fast diffing
Three Principles of Redux
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
Changes are made with pure functions - Why?
- Reducer functions are pure, meaning they always return the same result given the same inputs and have no side effects
- Composable
- combineReducers({filters: filtersReducer, list: listReducer})
- Reusable
- common pagination reducer for managing pagination state
- Just functions - meaning you can control the order of execution and pass additional data
React + Redux
Bringing two worlds together
Components
- How things look
- Nothing to do with redux
- Rely on props for data
- Call callbacks from props
Containers
- How things work
- Redux specific code
- Subscribe to the state
- Dispatch redux actions
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);
How does this work?
- A <Provider> element at the root of your application leverages React's context API to automatically pass the store to your components
- Containers take the store from the context and call your mapStateToProps() and mapDispatchToProps() methods
ReactDOM.render(
<Provider store={store}>
<Router>
...
</Router>
</Provider>,
document.getElementById('body-container')
);
Benefits of this separation
- Enables maximum reusability - components that look the same but work differently
- Example: buttons that send different actions
- Completely separates how a component looks from how it works
- focus on one thing at a time
- always know where to look when adjusting a component's behaviour
- no bloated components with too much logic
- Entire application is not rerendered on every store update
Summary of React + Redux
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
Usage in Large Apps
Techniques/Approaches for solving common problems with React + Redux in a large application
What are we going to cover?
- Route/page specific data
- Request management + avoiding race conditions
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.
Route/Page Specific Data
When you're building a single page app that's more than one page
Route Management
- React router
- react-router-redux
- page reducer (to hold the current page)
- page-specific reducers
React Router
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>
);
- Complete routing with hash history and pushState support
- Renders components based on matched pathnames
react-router-redux
// 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')
);
- Keeps router in sync with store state (and vice versa)
- Stores the current location information in the store
page reducer
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);
};
- Holds the state for the current page in the centralized store
- Dispatches to the correct page-specific reducer automatically
page-specific reducers
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,
};
},
});
- Inherently Reusable - know nothing about routing
- Simply models the page-specific data and nothing else
What does this give you?
- Automatically load/unload state when the page changes
- Clean way to structure page specific data reusably
- Method for incorporating route changes into your reducers
- Still allows for storing global state available for the entire application
Request Management
Taming asynchronicity and Race Conditions with Redux
The problem we are trying to solve
- Let's say you have a list of items
- You can filter those items based on several categories
- You can also search by typing into an input field
- Every time one of those filters changes, you want to go and fetch an updated list of items
- Web server processes requests in parallel
- Some requests are more expensive than others
The problem we are trying to solve
time
first request
(fairly expensive)
second request
(faster)
Outdated first request gets displayed because it comes back last
Comes back first
Race Condition
The solution
- redux-thunk - a plugin you don't need to know about to understand this
- Store each request grouped by some identifier
Example: for the list filter, we could call it "list-results" - When each request is sent out we increment a counter and use that value to determine if any further requests have been made
- This prevents outdated responses from triggering their actions and always ensures the most recent result is displayed
The solution
- We don't want to have to manually manage counters every time we need to make a request
- We want this to be globally stored so that no matter where the request is coming from, we favor the latest result
- Easy to do in redux: all state is stored in a single place anyway
sendRequest action creator
- Usage: instead of returning an object from your action creator, return sendRequest()
- Example:
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'};
},
});
};
What does this give you?
- A single action to send requests declaratively
- Handling each stage of the request lifecycle: begin, making the request, success, and failure
- Integration into Redux - actions returned by callbacks
- Eliminating race conditions: cancelling related requests when a more recent request is made
React + Redux is awesome
- React (quick overview)
- Redux
- Combing React + Redux
- Route/page specific data
- Request management & race conditions
Thanks!
Questions?
Sunjay Varma
Bringing Order to Large Applications with Redux
By Sunjay Varma
Bringing Order to Large Applications with Redux
- 1,150