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 is the "V" in MVC

React = React.js

facebook.github.io/react/

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,
};

Redux is Easy

(once you get it)

Redux = Redux.js

redux.js.org

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

Made with Slides.com