Redux Summary by

Redux is a small library that provides convenient functions to store application state in a single immutable state object. This read-only single source of truth is never changed directly; it’s mutated with pure functions called reducers.

Redux’s convenient functions in order of relevancy to this guide are:

Following the three principles of redux (single source of truth, read-only state, pure function mutations) offers several advantages:

  • your app is easier to debug and test (“you hit a weird edge case? Let’s just copy your state object and write a test against that weird state.”)
  • faster development cycles with time travel debugging
  • features like undo/redo and server-side rendering go from impossible to trivial
  • because mutations are centralized and are applied one by one, race condition bugs are much rarer
  • you can use some sick devtools to inspect your app with
  • the higher level patterns of passing state down in one direction and dispatching actions that modify an application wide state result in a clearer structure for building complicated apps

For example, the state of this app:

could be represented by this single object

const state = {
  filter: "ACTIVE",
  todos: [
    {
      id: 0,
      completed: false,
      text: "milk"
    },
    {
      id: 1,
      completed: false,
      text: "cookies"
    },
    {
      id: 2,
      completed: false,
      text: "eggs"
    },
    {
      id: 3,
      completed: true,
      text: "kale"
    }
  ]
}

From this state, it’s super clear what would need to happen to make any change to the app:

  • show only completed tasks? state.filter = "COMPLETED"
  • change “eggs” to “organic eggs”? state.todos.find(todo => todo.id === 2).text = "organic eggs"
  • delete “cookies”? state.todos = state.todos.filter(todo => todo.id !== 1)

Making your app render something is just a game of turning a piece of an immutable javascript object into UI. Making your app do something is just a game of mutating the correct piece of state.

Actions and Reducers

Instead of modifying the state directly (state.filter = "COMPLETED"), redux prescribes the more scalable solution of dispatching actions that get fed to reducers that do the actual modifying.

Actions

An action to change the filter to “COMPLETED” may look like:

const action = {
  type: "SET_FILTER",
  payload: {
    filter: "COMPLETED"
  }
}

Note: putting filter under payload instead of at the root of this action object follows the flux standard action guidelines

An action to add a todo may look like:

const action = {
  type: "ADD_TODO",
  payload: {
    text: "sausage",
    id: 5,
    completed: false
  }
}

Action Creators

Action Creators make it easier to generate actions. For the above ADD_TODO action, an action creator may look like:

const addTodo = todo => {
  return {
    type: "ADD_TODO",
    payload: todo
  }
}

Here’s an example of how to dispatch an action created by this action creator:

store.dispatch(addTodo({
  text: "almonds",
  id: 6,
  completed: false
}))

Reducers

A Redux Reducer is a function that receives 1) the app’s current state and 2) an action. Reducers return a new state modified by that action.

Reducers must be pure functions (functions that don’t have side effects like modifying their inputs and always return the same result with the same input). You can force yourself to make reducers pure functions by using a library like Immutable.js, but you’ll hit a lot of library incompatibility problems (ex: redux’s native combineReducers - explained below - needs to be replaced with a special redux-immutable library). If you’re starting out with redux, don’t use an extra library.

If a reducer receives an action it doesn’t know how to handle, it must return the untouched state. This makes reducers composable: multiple reducers can be combined and called in series.

If a reducer is called with a null or undefined state, it should provide a default state (which is easy to implement with ES6 default function parameters). This practice makes it easier for your app to start up.

Here’s an example reducer that could modify the application state in response to the SET_FILTER action above:

const filterReducer = (state = {}, { type, payload }) => {
  if(type === "SET_FILTER"){
    // make sure not to modify the original state
    const newState = {...state};
    newState.filter = payload.filter;
    return newState;
  }
  return state;
}

and another that could handle the ADD_TODO action:

const todosReducer = (state = {}, { type, payload }) => {
  if(type === "ADD_TODO"){
    const newState = {...state};
    newState.todos = [...newState.todos, payload];
    return newState;
  }
  return state;
}

and another that could handle both actions:

const rootReducer = (state = {}, { type, payload }) => {
  switch(type){
    case "ADD_TODO":
      const newTodos = [...state.todos, payload];
      return {...state, todos: newTodos};
    case "SET_FILTER":
      return {...state, filter: payload.filter};
    default: return state;
  }
}

CombineReducers

Using the shorthand combineReducers function produces an identical reducer in a more composable way:

// name this reducer with "Reducer" in the name
const todosReducer = (state = [], { type, payload }) => {
  switch(type){
    case "ADD_TODO": return [...state.todos, payload];
    default: return state;
  }
}

// this reducer has the same name as it's key in state
// this naming convention has an advantage you'll see below
const filter = (state = "", { type, payload }) => {
  switch(type){
    case "SET_FILTER": return payload.filter;
    default: return state;
  }
}

// combineReducers makes reducers more composable
const rootReducer = combineReducers(({
  todos: todosReducer,
  filter // cleaner
}));

Here’s a minimalistic implementation of combineReducers:

const combineReducers = (reducers) => {
  return (state = {}, action) => {
    // return reduced reducers, lol
    return Object.keys(reducers).reduce(
      (nextState, key) => {
        nextState[key] = reducers[key](
          state[key],
          action
        );
        return nextState;
      },
      {}
    );
  };
};

The redux-immutable library’s modified combineReducers effectively just changes the second line to:

  return (state = Immutable.Map, action)

Why not just modify the state directly?

The advantages of using actions and reducers instead of modifying that state directly are that:

  1. state is read only, which makes it easier to debug your apps and integrate tools (that’s how redux-devtools is possible)
  2. dispatching an action doesn’t require that you know how the state will be modified. You could change how the underlying state is structured:
const state = {
  filter: "ACTIVE",
  todos: {
    "0": {
      completed: false,
      text: "milk"
    },
    "2": {
      completed: false,
      text: "eggs"
    },
    "3": {
      completed: true,
      text: "kale"
    }
  }
}

and how your reducers change the state:

const todosReducer = (state = [], { type, payload }) => {
  switch(type){
    case "ADD_TODO": return {...state.todos, [payload.id]: payload};
    default: return state;
  }
}

but your actions wouldn’t need to change:

const action = {
  type: "ADD_TODO",
  payload: {
    text: "sausage",
    id: 5,
    completed: false
  }
}

Store

A redux store:

  1. holds the state
  2. holds your reducers
  3. let’s you dispatch actions through the reducers to modify the state

Here’s an example of how to create a redux store with Redux.createStore that holds a simple integer state and a simple increment/decrement reducer:

import { createStore } from "redux";

const reducer = (state = 0, { type }) => {
  switch(type){
    case "INCREMENT": return state + 1;
    case "DECREMENT": return state - 1;
    default: return state;
  }
}

// create a store with an optional initial state
const initialState = 10;
const store = createStore(reducer, initialState);

// listen for changes to state
const unsubscribe = store.subscribe(() => {
  // I'm not sure why state isn't given as an argument to this callback

  // get the updated state to re-render your UI
  console.dir(store.getState());
})

// dispatch an INCREMENT action, which triggers all subscriptions
store.dispatch({ type: "INCREMENT" });

// after the above INCREMENT action, state is changed:
store.getState() === 11

Simple createStore implementation

const createStore = (reducer, initialState) => {
  let state = initialState;
  let listeners = [];

  const getState = () => state;

  const dispatch = (action) => {
    state = reducer(state, action);
    listeners.forEach(listener => listener());
  };

  const subscribe = (listener) => {
    listeners.push(listener);
    // return an unsubscribe function
    return () => {
      listeners = listeners.filter(l => l !== listener);
    };
  };

  dispatch({});

  return { getState, dispatch, subscribe };
};

Compare this simple implementation with the actual Redux.createStore implementation, which adds enhancers and handles some weird edge cases.

Enhancers

Redux.createStore also accepts an optional enhancer.

TODO: more about enhancers


Sources:


Further reading: