Redux

 · 9 mins read

Redux is a predictable state container for JavaScript apps

Why?

As the requirements for JavaScript single-page applications have become increasingly complicated, our code must manage more state than ever before. This state can include server responses and cached data, as well as locally created data that has not yet been persisted to the server. UI state is also increasing in complexity, as we need to manage active routes, selected tabs, spinners, pagination controls, and so on.

Managing this ever-changing state is hard. If a model can update another model, then a view can update a model, which updates another model, and this, in turn, might cause another view to update. At some point, you no longer understand what happens in your app as you have lost control over the when, why, and how of its state. When a system is opaque and non-deterministic, it’s hard to reproduce bugs or add new features.

Redux attempts to make state mutations predictable by imposing certain restrictions on how and when updates can happen.

Three Principles

  • Single source of truth
  • State is read-only
  • Changes are made with pure functions

Prior Art

Flux

Can Redux be considered a Flux implementation?

Yes, and no.

Like Flux, Redux prescribes that you concentrate your model update logic in a certain layer of your application (“stores” in Flux, “reducers” in Redux).

Unlike Flux, Redux does not have the concept of a Dispatcher.

Another important difference from Flux is that Redux assumes you never mutate your data.

Basics

Actions

Actions are payloads of information that send data from your application to your store. They are the only source of information for the store. You send them to the store using store.dispatch().

Flux Standard Action

An action MUST

  • be a plain JavaScript object.
  • have a type property.

An action MAY

  • have an error property.
  • have a payload property.
  • have a meta property.

An action MUST NOT include properties other than type, payload, error, and meta.

type

The type of an action identifies to the consumer the nature of the action that has occurred. type is a string constant. If two types are the same, they MUST be strictly equivalent (using ===).

payload

The optional payload property MAY be any type of value. It represents the payload of the action. Any information about the action that is not the type or status of the action should be part of the payload field.

By convention, if error is true, the payload SHOULD be an error object. This is akin to rejecting a promise with an error object.

error

The optional error property MAY be set to true if the action represents an error.

An action whose error is true is analogous to a rejected Promise. By convention, the payload SHOULD be an error object.

If error has any other value besides true, including undefined and null, the action MUST NOT be interpreted as an error.

meta

The optional meta property MAY be any type of value. It is intended for any extra information that is not part of the payload.

/*
 * action types
 */

export const ADD_TODO = 'ADD_TODO'
export const TOGGLE_TODO = 'TOGGLE_TODO'
export const SET_VISIBILITY_FILTER = 'SET_VISIBILITY_FILTER'

/*
 * other constants
 */

export const VisibilityFilters = {
  SHOW_ALL: 'SHOW_ALL',
  SHOW_COMPLETED: 'SHOW_COMPLETED',
  SHOW_ACTIVE: 'SHOW_ACTIVE'
}

/*
 * action creators
 */

export function addTodo(text) {
  return { type: ADD_TODO, text }
}

export function toggleTodo(index) {
  return { type: TOGGLE_TODO, index }
}

export function setVisibilityFilter(filter) {
  return { type: SET_VISIBILITY_FILTER, filter }
}

Reducers

Reducers specify how the application’s state changes in response to actions sent to the store. Remember that actions only describe the fact that something happened, but don’t describe how the application’s state changes.

reducer signature:

(previousState, action) => newState

Things you should never do inside a reducer:

  • Mutate its arguments
  • Perform side effects like API calls and routing transitions;
  • Call non-pure functions, e.g. Date.now() or Math.random().
import { combineReducers } from 'redux'
import {
  ADD_TODO,
  TOGGLE_TODO,
  SET_VISIBILITY_FILTER,
  VisibilityFilters
} from './actions'
const { SHOW_ALL } = VisibilityFilters

function visibilityFilter(state = SHOW_ALL, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return action.filter
    default:
      return state
  }
}

function todos(state = [], action) {
  switch (action.type) {
    case ADD_TODO:
      return [
        ...state,
        {
          text: action.text,
          completed: false
        }
      ]
    case TOGGLE_TODO:
      return state.map((todo, index) => {
        if (index === action.index) {
          return Object.assign({}, todo, {
            completed: !todo.completed
          })
        }
        return todo
      })
    default:
      return state
  }
}

const todoApp = combineReducers({
  visibilityFilter,
  todos
})

/* this is equivalent
const todoApp = function reducer(state = {}, action) {
  return {
    visibilityFilter: visibilityFilter(state.visibilityFilter, action),
    todos: todos(state.todos, action)
  }
}
*/

export default todoApp

Store

In the previous sections, we defined the actions that represent the facts about “what happened” and the reducers that update the state according to those actions.

The Store is the object that brings them together. The store has the following responsibilities:

  • Holds application state;
  • Allows access to state via getState();
  • Allows state to be updated via dispatch(action);
  • Registers listeners via subscribe(listener);
  • Handles unregistering of listeners via the function returned by subscribe(listener).

It’s important to note that you’ll only have a single store in a Redux application. When you want to split your data handling logic, you’ll use reducer composition instead of many stores.

It’s easy to create a store if you have a reducer. In the previous section, we used combineReducers() to combine several reducers into one. We will now import it, and pass it to createStore().

import { createStore } from 'redux'
import todoApp from './reducers'

let store = createStore(todoApp)


/*
You may optionally specify the initial state as the second argument to createStore(). 
This is useful for hydrating the state of the client to match the state of a Redux application running on the server.
*/
let store = createStore(todoApp, window.STATE_FROM_SERVER)

Data Flow

Redux architecture revolves around a strict unidirectional data flow.

The data lifecycle in any Redux app follows these 4 steps:

1.You call store.dispatch(action).

 { type: 'LIKE_ARTICLE', articleId: 42 }
 { type: 'FETCH_USER_SUCCESS', response: { id: 3, name: 'Mary' } }
 { type: 'ADD_TODO', text: 'Read the Redux docs.' }

2.The Redux store calls the reducer function you gave it.

// Your reducer returns the next application state
 let nextState = todoApp(previousState, action)

3.The root reducer may combine the output of multiple reducers into a single state tree.

How you structure the root reducer is completely up to you. Redux ships with a combineReducers() helper function, useful for “splitting” the root reducer into separate functions that each manage one branch of the state tree.

Here’s how combineReducers() works. Let’s say you have two reducers, one for a list of todos, and another for the currently selected filter setting:

function todos(state = [], action) {
   // Somehow calculate it...
   return nextState
 }

 function visibleTodoFilter(state = 'SHOW_ALL', action) {
   // Somehow calculate it...
   return nextState
 }

 let todoApp = combineReducers({
   todos,
   visibleTodoFilter
 })

When you emit an action, todoApp returned by combineReducers will call both reducers:

let nextTodos = todos(state.todos, action)
 let nextVisibleTodoFilter = visibleTodoFilter(state.visibleTodoFilter, action)

It will then combine both sets of results into a single state tree:

return {
   todos: nextTodos,
   visibleTodoFilter: nextVisibleTodoFilter
 }

While combineReducers() is a handy helper utility, you don’t have to use it; feel free to write your own root reducer!

4.The Redux store saves the complete state tree returned by the root reducer.

This new tree is now the next state of your app! Every listener registered with store.subscribe(listener) will now be invoked; listeners may call store.getState() to get the current state.

Now, the UI can be updated to reflect the new state. If you use bindings like React Redux, this is the point at which component.setState(newState) is called.

Using with React

Installing React Redux

npm install --save react-redux

Presentational and Container Components

If you’re not familiar with these terms, read about them first, and then come back. They are important, so we’ll wait!

Most of the components we’ll write will be presentational, but we’ll need to generate a few container components to connect them to the Redux store.

Technically you could write the container components by hand using store.subscribe(). We don’t advise you to do this because React Redux makes many performance optimizations that are hard to do by hand. For this reason, rather than write container components, we will generate them using the connect() function provided by React Redux, as you will see below.

This part contains large chunks of codes and explanations, so see more detail at here

And for more advanced information, I wanna talk later…


Recipes:

Using Object Spread Operator

Since one of the core tenets of Redux is to never mutate state, you’ll often find yourself using Object.assign() to create copies of objects with new or updated values.

An alternative approach is to use the object spread syntax proposed for the next versions of JavaScript which lets you use the spread (…) operator to copy enumerable properties from one object to another in a more succinct way. The object spread operator is conceptually similar to the ES6 array spread operator.

function todoApp(state = initialState, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return Object.assign({}, state, {
        visibilityFilter: action.filter
      })
    default:
      return state
  }
}

//this is equivalent to 

function todoApp(state = initialState, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return { ...state, visibilityFilter: action.filter }
    default:
      return state
  }
}

LINKS:

Redux

Flux Standard Action