Redux is a predictable state container for JavaScript apps.

These examples come from our work on Talk in 2016. They walk through the underlying concepts by hand; see the update at the bottom for the recommended starting point today.

To understand Redux we need to dive into a few concepts:

  • Actions
  • Action Creators
  • Action Types
  • Reducers
  • Stores

The three principles

These are the three principles to build applications with Redux. The following are specified in the Redux Documentation

Single source of truth

The state managed by Redux is stored in an object tree within a single store. This does not mean that every piece of local UI state needs to move into Redux. In our first example, we’ll represent the store’s state with a JavaScript object.

State is read-only

The only way to change 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.

Actions

Actions describe that something happened in our application. They are payloads of information that send data to your store. They are the only source of information for the store.

Here is an example:

JavaScript
const ADD_COMMENT = 'ADD_COMMENT';

const action = {
  type: ADD_COMMENT,
  comment: 'This is my comment.'
};

Actions are JavaScript objects. Every action must have a type property that indicates the type of action being performed. In these examples, we define types as constants so they are easy to reuse. This is a convention, not a requirement.

Once an app becomes big enough, you may want to move them into a separate module. We store them in a constants.js file. auth.js Constants

JavaScript
import { ADD_COMMENT, REMOVE_COMMENT } from "./constants";

We can dispatch an action by using dispatch().

Our actions live within the coral-framework/actions folder. talk/client/coral-framework/actions

More about Actions: Actions · Redux

Async Actions

For our async operations we use a convention of dispatching three kinds of actions. Redux does not send the request or generate these actions automatically: the network call happens outside the reducer, for example in a thunk, which dispatches request and success or failure actions.

  • <ACTION_TYPE>_REQUEST

  • <ACTION_TYPE>_SUCCESS

  • <ACTION_TYPE>_FAILURE

Request

We use the postfix _REQUEST to know that the resource is being requested.

Success

We use the postfix _SUCCESS to know that the resource response came back successfully.

Failure

We use the postfix _FAILURE to know that the resource request failed.

Action Creators

Action Creators are functions that return actions. This makes it easier to use, portable and testable.

JavaScript
function addComment(comment) {
  return {
    type: ADD_COMMENT,
    comment,
  };
}

So we can later trigger those actions by using dispatch()

JavaScript
dispatch(addComment(comment));

Dispatch Function

The dispatch() function can be accessed directly from the store as store.dispatch(), but more likely you'll access it using a helper like react-redux's connect().

In Talk, our container components use connect() to read store state and receive functions that dispatch actions.

Reducers

With Actions we describe that something happened in our application. But we don’t specify how our state will be modified with this change.

In a Reducer we will specify how the state of our application changes when an action has been dispatched.

Here we also will want to specify the initialState

Before building reducers it’s important that you:

  • Don’t mutate the state.
  • Return the previous state in the default case.

Here is an example for auth.js. Define the action types in constants.js first:

JavaScript
export const CHECK_LOGIN_REQUEST = "CHECK_LOGIN_REQUEST";
export const CHECK_LOGIN_SUCCESS = "CHECK_LOGIN_SUCCESS";
export const CHECK_LOGIN_FAILURE = "CHECK_LOGIN_FAILURE";
JavaScript
import * as actions from "./constants";

const initialState = {
  isLoading: false,
  loggedIn: false,
  user: null,
  error: "",
};

export default function auth(state = initialState, action) {
  switch (action.type) {
    case actions.CHECK_LOGIN_REQUEST:
      return Object.assign({}, state, {
        isLoading: true,
      });
    case actions.CHECK_LOGIN_SUCCESS:
      return Object.assign({}, state, {
        isLoading: false,
        loggedIn: true,
        user: action.user,
        error: "",
      });
    case actions.CHECK_LOGIN_FAILURE:
      return Object.assign({}, state, {
        isLoading: false,
        error: action.error,
        loggedIn: false,
        user: null,
      });
    default:
      return state;
  }
}

Notice that a reducer takes state as its first argument and uses initialState when that argument is undefined. It then handles the action, or returns the current state in the default case. As a second argument it takes the action. We have our state and we have the action. This is the time to specify how we modify the state.

Reducers using ImmutableJS

In Talk we used ImmutableJS to maintain our app state. Here is a guide on how to use ImmutableJS. It is an option, not a Redux requirement.

This is how a simplified version of our auth reducer looks like:

JavaScript
import { Map } from "immutable";
import * as actions from "./constants";

const initialState = Map({
  isLoading: false,
  loggedIn: false,
	user: null,
	error: ''
});

export default function auth(state = initialState, action) {
  switch (action.type) {
    case actions.CHECK_LOGIN_REQUEST:
    	return state
      	.set('isLoading', true);
    case actions.CHECK_LOGIN_SUCCESS:
    	return state
      	.set('isLoading', false)
      	.set('loggedIn', true)
      	.set('user', action.user)
      	.set('error', '');
    case actions.CHECK_LOGIN_FAILURE:
    	return state
      	.set('isLoading', false)
			.set('error', action.error)
			.set('loggedIn', false)
			.set('user', null);
    default:
      return state
  }
}

Looks cleaner, right?

It’s pretty easy to follow. Here it says if a CHECK_LOGIN_REQUEST action has been dispatched set the isLoading from our state to true. And we can show a tiny loader to let the user know we are requesting something to the server.

Our reducers live within the coral-framework/reducers folder. talk/client/coral-framework/reducers

More about Reducers: Reducers · Redux

And the last thing we need to see is the Store

Store

The Store is what holds the application state. Here we can access and update the state.

It’s important to note that we will only have a single store in our application, created from our rootReducer and we will use reducer composition instead of many stores.

Here is an example of how create a store with createStore() using a reducer:

JavaScript
import { createStore } from "redux";
import authReducer from "./auth";
const store = createStore(authReducer);

// Using the plain-object reducer from auth.js above:
store.dispatch({ type: "CHECK_LOGIN_REQUEST" });
store.getState().isLoading; // true

store.dispatch({
  type: "CHECK_LOGIN_SUCCESS",
  user: { id: "1", name: "Bel" }
});
store.getState().isLoading; // false
store.getState().loggedIn; // true
store.getState().user.name; // "Bel"

That is the full loop: dispatch an action, let the reducer calculate the next state, and read the result from the store. With the ImmutableJS version, use .get("isLoading") instead of .isLoading, and likewise for the other fields.

In Talk we have multiple reducers. The following excerpt assumes each reducer is defined in its own module, so we combine them with combineReducers() within a single store

JavaScript
import {combineReducers} from 'redux';

import authReducer from './auth'
import configReducer from './config'
import userReducer from './user'

const rootReducer = combineReducers({
  authReducer,
  configReducer,
  userReducer
});

More about Stores: Store · Redux

Useful Resources

Redux Documentation · Redux Getting Started with Redux Usage with React · Redux