Redux
Redux is a predictable state container for JavaScript apps.
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 of your whole application is stored in an object tree within a single store. We are going to represent the whole state of our application in a single 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';
{
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. Types should be defined as constants.
Once an app becomes big enough, you may want to move them into a separate module. We store them in a contants.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 dispatch three 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(removeComment(comment.id));
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'sconnect()
.
We use connect()
in our containers. More about this in Architecture.
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 change when an action has been dispatched.
Here we also will want to specify the initialState
Before building reducers it’s important to that you: - Don’t mutate the state - Return the previous state in the default case.
Here is an example of an auth reducer:
const initialState = {
isLoading: false,
loggedIn: false,
user: null,
error: "",
};
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 the state
as first argument and when it’s not defined it returns the initialState
. 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
We are using ImmutableJS to maintain our app state. Here is a guide on how to use ImmutableJS.
This is how a simplified version of our auth reducer looks like:
const initialState = Map({
isLoading: false,
loggedIn: false,
user: null,
error: ‘’
});
function auth (state = initialState, action) {
switch (action.type) {
case CHECK_LOGIN_REQUEST:
return state
.set('isLoading', true);
case CHECK_LOGIN_SUCCESS:
return state
.set('isLoading', false)
.set('loggedIn', true)
.set('user', action.user)
.set('error', '');
});
case 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 now we are requesting something to the server.
Our actions 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 called 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";
let store = createStore(authReducer);
We do have a lot of stores so we will need to combine all our reducers 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