React + Redux

  • Flux is the application architecture that Facebook uses for building client-side web applications.
  • It complements React's composable view components by utilizing a unidirectional data flow.
  • It's more of a pattern rather than a formal framework, and you can start using Flux immediately without a lot of new code.

Flux

the primary mental model for the Flux programmer.

Structure and Data Flow

Redux

A predictable state container for JavaScript apps

Why Redux?

Hard Problems

  • Mutable State
  • Race conditions
  • Listening for state change
  • Interface for state change

What is Redux?

  • Defines How Data Flows
  • Creates a Single Source of Truth
  • Creates a Single Source of Change
  • Forces a Discretely Testable Mutation Functions
  • Redux is a pattern and library for managing and updating application state, using events called "actions".
  • Redux helps you manage "global" state - state that is needed across many parts of your application.
  • The patterns and tools provided by Redux make it easier to understand when, where, why, and how the state in your application is being updated, and how your application logic will behave when those changes occur.

Redux Constraints (Principles)

  • Single State Tree

  • Actions Describe Updates

  • Reducers (Pure Functions) Apply Updates

Redux Contracts

  • Reducers

  • Selectors

  • Middleware

  • Enhancers

Reducer

(state, action) => state

The selector pattern solves the problem of how the application should fetch values from this centralized state.

Selectors

(state, ...args) => derivation
const getVisibleTodos = (todos, filter) => {
  switch (filter) {
    case 'SHOW_ALL':
      return todos
    case 'SHOW_COMPLETED':
      return todos.filter(t => t.completed)
    case 'SHOW_ACTIVE':
      return todos.filter(t => !t.completed)
  }
}

Middleware

store => next => action => any

const logger = store => next => action => {
  console.log('dispatching', action)
  let result = next(action)
  console.log('next state', store.getState())
  return result
}

Enhancers

createStore => createStore

A store enhancer is a higher-order function that composes a store creator to return a new, enhanced store creator.

The Redux DevTools platform is one such store enhancer.

Single Source of Truth

Redux uses only one store for all its application state. Since all state resides in one place, Redux calls this the single source of truth.

State is Read-Only

According to Redux docs,

"The only way to mutate the state is to emit an action, an object describing what happened."

This means the application cannot modify the state directly. Instead, "actions" are dispatched to express an intent to change the state in the store.

The store object itself has a very small API with only four methods:

  • store.dispatch(action)

  • store.subscribe(listener)

  • store.getState()

  • replaceReducer(nextReducer)

var action = {
  type: 'ADD_USER',
  user: {name: 'Dan'}
};

Changes are made with Pure Functions

  •  The dispatched action "describes" the state change and an intent to change state.

  • Reducers are functions that you write which handle dispatched actions and can actually change the state.

  • A reducer takes in current state as an argument and can only modify the state by returning new state.

Reducers are called "pure" because they do nothing but return a value based on their parameters. They have no side effects into any other part of the system.

Redux Flow

First Redux Store

// The Reducer Function
var userReducer = function(state, action) {
  if (state === undefined) {
    state = [];
  }
  if (action.type === 'ADD_USER') {
    state.push(action.user);
  }
  return state;
}

// Create a store by passing in the reducer
var store = Redux.createStore(userReducer);

// Dispatch our first action 
// to express an intent to change the state
store.dispatch({
  type: 'ADD_USER',
  user: {name: 'Dan'}
});

Here's a brief summary of what's happening:

  1. The store is created with one reducer.
  2. The reducer establishes that the initial state of the application is an empty array. *
  3. A dispatch is made with a new user in the action itself
  4. The reducer adds the new user to the state and returns it, which updates the store.

Don't Mutate State, Copy It

  • Even though reducers are responsible for changing state, they should never mutate the "current state" argument directly.
  • This is why we shouldn't use .push(), a mutation method, on the state argument of the reducer.
const userReducer = function(state = [], action) {
  if (action.type === 'ADD_USER') {
    var newState = state.concat([action.user]);
    return newState;
  }
  return state;
}

Multiple Reducers

  • Since Redux uses just one store, we'll need to use nested objects to organize state into different sections.
  • Let's imagine we want our store to resemble this object:
{
  userState: { ... },
  widgetState: { ... }
}

In order to create a store with nested objects, we'll need to define each section with a reducer:

It's still "one store = one object" for the entire application, but it has nested objects for userState and widgetState that can contain all kinds of data.

import { createStore, combineReducers } from 'redux';

// The User Reducer
const userReducer = function(state = {}, action) {
  return state;
}

// The Widget Reducer
const widgetReducer = function(state = {}, action) {
  return state;
}

// Combine Reducers
const reducers = combineReducers({
  userState: userReducer,
  widgetState: widgetReducer
});

const store = createStore(reducers);

 Then the state returned from each reducer applies to its subsection.

Something very important to note is that now, each reducer gets passed its respective subsection of the overall state, not the whole store's worth of state like with the one-reducer example.

Which Reducer is Called After a Dispatch?

All of them.

Initial State and Time Travel

  • If you read the docs, you may notice a second argument for createStore() which is for "initial state".
  • This might seem like an alternative to reducers creating initial state.
  • However, this initial state should only be used for "state hydration".
  • This brings up an interesting concept though.
  • If it's so cheap and easy to rehydrate old state, one could imagine the equivalent of state "time travel" in their app.
  • This can be useful for debugging or even undo/redo features.

Redux with React

  • Redux is framework-agnostic.

Component without Redux

import React from 'react';
import axios from 'axios';
import UserList from '../views/list-user';

class UserListContainer extends React.Component {
  constructor() {
    this.state = {
        users: []
    };
  }
  componentDidMount() {
    axios.get('/path/to/user-api').then(response => {
      this.setState({users: response.data});
    });
  }
  render() {
    return <UserList users={this.state.users} />;
  }
};

Connecting with react-redux

The react-redux module allows us to "connect" React components to Redux in a more convenient way.

import { connect } from 'react-redux';
import store from '../path/to/store';

class UserListContainer extends React.Component {
  componentDidMount() {
    axios.get('/path/to/user-api').then(response => {
      store.dispatch({
        type: 'USER_LIST_SUCCESS',
        users: response.data
      });
    });
  }
  render() {
    return <UserList users={this.props.users} />;
  }
};

const mapStateToProps = function(store) {
  return {
    users: store.userState.users
  };
}

export default connect(mapStateToProps)(UserListContainer);

Provider

  • In order for any of this react-redux code to work, you'll need to let your app know how to use react-redux with a <Provider /> component.
  • This component wraps your entire React application.
  • If you're using React Router, it would look like this.
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import store from './store';
import router from './router';

ReactDOM.render(
  <Provider store={store}>{router}</Provider>,
  document.getElementById('root')
);
const initialUserState = {
  users: []
}

const userReducer = function(state = initialUserState, action) {
  switch(action.type) {
  case 'USER_LIST_SUCCESS':
    return Object.assign({}, state, { users: action.users });
  }
  return state;
}

Reducer for previous example

Dispatching from Events

const mapDispatchToProps = function(dispatch, ownProps) {
  return {
    toggleActive: function() {
      dispatch({ ... });
    }
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(UserListContainer);

When should I use Redux instead?

  • You have larger amounts of application state that are needed in many places in the app
  • The app state is updated frequently over time
  • The logic to update that state may be complex
  • The app has a medium or large-sized codebase, and might be worked on by many people
  • You want to be able to understand when, why, and how the state in your application has updated, and visualize the changes to your state over time
  • You need more powerful capabilities for managing side effects, persistence, and data serialization

A Case Study

Middleware

  • It provides a third-party extension point between dispatching an action, and the moment it reaches the reducer.
  • If you've used server-side libraries like Express and Koa, you were also probably already familiar with the concept of middleware.
  • Redux middleware solves different problems than Express or Koa middleware, but in a conceptually similar way.

Problem: Logging

  • Every time an action is dispatched, the new state is computed and saved.
  • The state cannot change by itself, it can only change as a consequence of a specific action.
  • Wouldn't it be nice if we logged every action that happens in the app, together with the state computed after it?
  • When something goes wrong, we can look back at our log, and figure out which action corrupted the state.

Attempt #1: Logging Manually

The most naïve solution is just to log the action and the next state yourself every time you call store.dispatch(action).

store.dispatch(addTodo('Use Redux'))
const action = addTodo('Use Redux')

console.log('dispatching', action)
store.dispatch(action)
console.log('next state', store.getState())

This produces the desired effect, but you wouldn't want to do it every time.

Attempt #2: Wrapping Dispatch

You can extract logging into a function:

function dispatchAndLog(store, action) {
  console.log('dispatching', action)
  store.dispatch(action)
  console.log('next state', store.getState())
}

You can then use it everywhere instead of store.dispatch():

dispatchAndLog(store, addTodo('Use Redux'))

Attempt #3: Monkeypatching Dispatch

  • What if we just replace the dispatch function on the store instance?
  • The Redux store is just a plain object with a few methods, and we're writing JavaScript, so we can just monkeypatch the dispatch implementation:
const next = store.dispatch
store.dispatch = function dispatchAndLog(action) {
  console.log('dispatching', action)
  let result = next(action)
  console.log('next state', store.getState())
  return result
}
  • This is already closer to what we want! No matter where we dispatch an action, it is guaranteed to be logged.
  • Monkeypatching never feels right, but we can live with this for now.

Problem: Crash Reporting

  • What if we want to apply more than one such transformation to dispatch?
  • Wouldn't it be useful if, any time an error is thrown as a result of dispatching an action, we would send it to a crash reporting service like Sentry with the stack trace, the action that caused the error, and the current state?
  • This way it's much easier to reproduce the error in development.

If logging and crash reporting are separate utilities, they might look like this:

function patchStoreToAddLogging(store) {
  const next = store.dispatch
  store.dispatch = function dispatchAndLog(action) {
    console.log('dispatching', action)
    let result = next(action)
    console.log('next state', store.getState())
    return result
  }
}

function patchStoreToAddCrashReporting(store) {
  const next = store.dispatch
  store.dispatch = function dispatchAndReportErrors(action) {
    try {
      return next(action)
    } catch (err) {
      console.error('Caught an exception!', err)
      Raven.captureException(err, { extra: { action, state: store.getState()}})
      throw err
    }
  }
}

patchStoreToAddLogging(store)
patchStoreToAddCrashReporting(store)

Attempt #4: Hiding Monkeypatching

  • Monkeypatching is a hack.
  • “Replace any method you like”, what kind of API is that?
  • Let's figure out the essence of it instead.
  • Previously, our functions replaced store.dispatch.
  • What if they returned the new dispatch function instead?
function logger(store) {
  const next = store.dispatch

  // Previously:
  // store.dispatch = function dispatchAndLog(action) {

  return function dispatchAndLog(action) {
    console.log('dispatching', action)
    let result = next(action)
    console.log('next state', store.getState())
    return result
  }
}
function applyMiddlewareByMonkeypatching(store, middlewares) {
  middlewares = middlewares.slice()
  middlewares.reverse()

  // Transform dispatch function with each middleware.
  middlewares.forEach(middleware => 
    (store.dispatch = middleware(store)))
}

We could use it to apply multiple middleware like this:

applyMiddlewareByMonkeypatching(store, [logger, crashReporter])
  • However, it is still monkeypatching.
  • The fact that we hide it inside the library doesn't alter this fact.

Attempt #5: Removing Monkeypatching

  • Why do we even overwrite dispatch?
  • Of course, to be able to call it later, but there's also another reason: so that every middleware can access (and call) the previously wrapped store.dispatch:
function logger(store) {
  // Must point to the function returned 
  // by the previous middleware:
  const next = store.dispatch

  return function dispatchAndLog(action) {
    console.log('dispatching', action)
    let result = next(action)
    console.log('next state', store.getState())
    return result
  }
}
  • If applyMiddlewareByMonkeypatching doesn't assign store.dispatch immediately after processing the first middleware, store.dispatch will keep pointing to the original dispatch function.
  • Then the second middleware will also be bound to the original dispatch function.
  • But there's also a different way to enable chaining.
  • The middleware could accept the next() dispatch function as a parameter instead of reading it from the store instance.
function logger(store) {
  return function wrapDispatchToAddLogging(next) {
    return function dispatchAndLog(action) {
      console.log('dispatching', action)
      let result = next(action)
      console.log('next state', store.getState())
      return result
    }
  }
}
  • The function cascade feels intimidating.
  • ES6 arrow functions make this currying easier on eyes:
const logger = store => next => action => {
  console.log('dispatching', action)
  let result = next(action)
  console.log('next state', store.getState())
  return result
}
  • This is exactly what Redux middleware looks like.
  • Now middleware takes the next() dispatch function, and returns a dispatch function, which in turn serves as next() to the middleware to the left, and so on.
  • It's still useful to have access to some store methods like getState(), so store stays available as the top-level argument.

Attempt #6: Naïvely Applying the Middleware

// Warning: Naïve implementation!
// That's *not* Redux API.
function applyMiddleware(store, middlewares) {
  middlewares = middlewares.slice()
  middlewares.reverse()
  let dispatch = store.dispatch
  middlewares.forEach(middleware => (dispatch = middleware(store)(dispatch)))
  return Object.assign({}, store, { dispatch })
}

The implementation of applyMiddleware() that ships with Redux is similar, but different in some important aspects.

Async Actions

When you call an asynchronous API, there are two crucial moments in time: the moment you start the call, and the moment when you receive an answer (or a timeout).

  • Each of these two moments usually require a change in the application state; to do that, you need to dispatch normal actions that will be processed by reducers synchronously.
  •  Usually, for any API request you'll want to dispatch at least three different kinds of actions:
  • An action informing the reducers that the request began.
  • An action informing the reducers that the request finished successfully.
  • An action informing the reducers that the request failed.
{ type: 'FETCH_POSTS' }
{ type: 'FETCH_POSTS', status: 'error', error: 'Oops' }
{ type: 'FETCH_POSTS', status: 'success', response: { ... } }
{ type: 'FETCH_POSTS_REQUEST' }
{ type: 'FETCH_POSTS_FAILURE', error: 'Oops' }
{ type: 'FETCH_POSTS_SUCCESS', response: { ... } }
  • The standard way to do it with Redux is to use the Redux Thunk middleware.
  • It comes in a separate package called redux-thunk.
function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => next => action => {
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }

    return next(action);
  };
}
  • When an action creator returns a function, that function will get executed by the Redux Thunk middleware.
  • This function doesn't need to be pure; it is thus allowed to have side effects, including executing asynchronous API calls.
  • The function can also dispatch actions—like those synchronous actions we defined earlier.
import fetch from 'cross-fetch'

export const INVALIDATE_SUBREDDIT = 'INVALIDATE_SUBREDDIT'
export function invalidateSubreddit(subreddit) {
  return {
    type: INVALIDATE_SUBREDDIT,
    subreddit
  }
}

export function fetchPosts(subreddit) {
  return function(dispatch) {
    dispatch(requestPosts(subreddit))
    return fetch(`https://www.reddit.com/r/${subreddit}.json`)
      .then(
        response => response.json(),
        error => console.log('An error occurred.', error))
      .then(json =>
        dispatch(receivePosts(subreddit, json))
      )
  }
}

store.dispatch(selectSubreddit('reactjs'))

deck

By Arfat Salman