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:
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
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.
function addComment(comment) {
return {
type: ADD_COMMENT,
comment,
};
}
So we can later trigger those actions by using dispatch()
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:
export const CHECK_LOGIN_REQUEST = "CHECK_LOGIN_REQUEST";
export const CHECK_LOGIN_SUCCESS = "CHECK_LOGIN_SUCCESS";
export const CHECK_LOGIN_FAILURE = "CHECK_LOGIN_FAILURE";
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:
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:
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
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