State management with Redux

After I finished the revamp of Hexo Node Admin, I did not feel too good about how the project was organized. Quite a few reused components were not stripped out, business logic was mixed with UI definitions. The whole component seemed to be bloated. In order to extract the business logic out of the component as much as possible, I decided to turn to Redux for help.

Redux is a Javascript state management tool. It provides a predictable way to manage the states of your app as well as the transitions between different states in a universal way. Since it is a general library and it does not only work on React. You can use it with any JS front end framework or just vanilla JS. By using such tool, the app is less prone to bugs because the states and the transitions are well contained and it’s much easier to debug. Redux has a Dev Tool browser extension which is extremely useful to track state changes.

There are quite some new concepts that I have to take in before writing any code. Typically, by using Redux, you will need to define some action types, some actions creators, some reducers and a store for the states. The basic logic here is that any state change can only be initiated by an action. So that the development of state is linear and predictable. Action types are simply enums for you to distinguish between different actions. For example:

1
2
3
4
// Login
export const SIGN_IN = "SIGN_IN";
export const SIGN_OUT = "SIGN_OUT";
export const INVALIDATE_TOKEN = "INVALIDATE_TOKEN";

Action type is the only trait used for determining which action is taken so they should be unique. Action creators are not necessary, you can always create an action without creators. Here is an example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*
An action is basically an object like this
{
type: SIGN_IN,
payload: { username: "", password: "" } // if needed
}
*/

const signIn = (username, password) => (
{
type: SIGN_IN,
payload: { username, password }
}
) // This is just a simple way of writing a function that returns an object (by wrapping the object in parentheses).

Action creator makes things easier by creating actions in a more formatted way especially for actions with payload (extra data that are necessary for determining next state) and will be often used in practice, though not a must. A reducer is a function that determines what our state should look like given current state and the action that has been taken.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const initialState = {
username: "",
password: ""
}

const reducer = (state = initialState, action) => {
switch (action.type) { // Action types that we have defined
case SIGN_IN:
return {
username: action.payload.username,
password: action.payload.password // payload is a common name given to extra information on the action, you can use whatever name you like
}
case default:
return state; // If no action, state remains unchanged.
}
}

A store is an instance to store the state and how state transitions.

1
2
3
4
5
import { createStore } from 'redux';

const store = createStore(reducer); // Using combineReducers can combine multiple reducers into one store

export default store;

Now that we have the store, we can dispatch an action when we need to and subscribe to the store to watch for changes that will trigger UI re-render.

1
2
3
4
5
store.dispatch(signIn("test", "test"));

store.subscribe(() => {
// State changed, do stuff
})

React Redux provides useful bindings for Redux on React components to make the state and the actions available to components in an easy and progressing way. Normal steps for composing Redux components are:

  1. Wrap your component in <Provider /> component imported from React Redux.
  2. import connect from React Redux.
  3. write mapStateToProps which maps state to props that are fed to your component. This is not a must but quite useful if you only want relevant states passed to the component instead of the whole store or you want to do some transformation on the data before sending to the component.
  4. write mapDispatchToProps which maps actions to function props that are fed to your component. This is not a must either, but also quite useful to directly add functions in props that can be called to trigger actions. If not specified, the dispatch function will be passed and you need to manually call dispatch(action) to trigger actions which can be verbose.
  5. Connect your component with store. export default connect(mapStateToProps, mapDispatchToProps)(Component)

Read the documentation for more details. Last thing I want to mention is async actions. Async actions are actions that pose side-effects on your component, in most cases, network requests. Remember when we defined actions earlier, the action was just a simple object with type and payload right? Async actions define how we get the payload that came from a side-effect event (network request). redux-thunk is a widely used middleware for Redux to help define async actions, the actions are now functions instead of plain objects and they are often referred to as thunk. By using redux-thunk, we return functions that define how we would get (compute) payload and dispatch the action.

1
2
3
4
5
6
7
export const getPost = (postId) => dispatch => {
fetch(`/api/post/${postId}`)
.then(res => res.json())
.then((data) => {
dispatch({ action: GET_POST, payload: { data } }); // GET_POST is an action type that we define
})
}

This is the basics of Redux and how to use Redux with React. My feeling is that it’s very important to find the balance between global states and local states (such as whether a dialog should open). You do not always need Redux, in fact, in many cases, it even makes the code more obscure because you have to write a lot of boilerplate code to get Redux working.