Analyze Redux

TOAST UI
8 min readMay 17, 2018

--

Redux is a state management container, implemented as inspired by Event Sourcing patterns and Functional programming. It helps to save and easily predict application states, so as to maintain consistency in implementation. As a result, it is simple and easy to test, maintain and debug.

Redux has following features:

  • The fundamental principles of Redux are Single Source of Truth (SSOT), Read-only State, and Changes from Pure Function.
  • Three core concepts of Redux are Action, Reducer, and Store.

JavaScript application developers were thrilled by these features. Although Redux is not a whole new concept, it is the library that is made for easy use based on the concepts. In this document, we’d like to look into more details of the flow, from Action => Store => Reducer => Result by the code level.

Introduction

Before getting into Redux, we need to understand its principles and concepts (refer to official Redux documentation for more details).

Three Fundamental Principles

  1. Single source of truth (SSOT)
  2. Read-only state
  3. Changes from pure functions

Redux manages all application data in the read-only state. And the state can be changed from pure functions. It might sound contradictory to change read-only state with a pure function. This means, when it requires to change data, always make the whole state new, to replace the previous state. In short, each state can be considered as a frame of a movie.

Core concepts

  • Action: Describes what happened in an application. Must contain a type property as a plain object.
  • Reducer: A function that returns the next state of an application: process the previous state and action and returns the next state.
  • Store: An object providing API that saves and allows to read an application state, sending actions to reducers or detecting state changes.

Redux provides a store, as an abstract concept of application state management, and manages state tree internally. An application dispatches an action that describes what happened. A store restructures and replaces a state tree through a reducer, and informs the changes to the application again. Then, the application executes UI changes or other service logics.

// A reducer is a pure function, in the form of 
// `(state, action) => state`.
// Gets and processes actions that trigger state changes,
// and returns a new state.
// A new state has different references from the previous state.
currentState = currentReducer(currentState, action);

Store

A store manages reducers, application states, event listeners, and a value representing dispatching (isDispatching: Boolean). It also exposes APIs for dispatch, subscribe, getState, replaceReducer, externally.

Redux follows a unidirectional data flow, like below (refer to: Redux Data Flow).

Here’s a simple implementation of a store(createStore). Once you understand the unidirectional data flow, it won’t be too difficult.

Stores have reduced costs for slices by separating change states of the listener list into nextListeners and currentListeners . The implementation codes are available at “Delay copying listeners util is necessary”.

The createStore function can be applied with Higher-Order Function, which is called enhancer in Redux. Its application goes as below:

function enhancer(createStore) {
//...
return function(reducer, initialState) {
// ...
return createStore(reducer, initialState);
}
}
const createStoreEnhanced = enhancer(createStore);
const enhancedStore = createStoreEnhanced(reducer, initialState);
// or use enhancer as a parameter as below
const enhancedStore = createStore(reducer, initialState, enhancer);

And, Redux provides an enhancer called applyMiddleware . Middleware will be described further in the document.

Compose

Before Middleware, let’s learn about compose, which is provided as a main API by Redux. compose provides a quite amount of convenience in developing applications.

If functions are named f, g, and h, and the entry is to be pipelined in the order of h, g, and f, following format can be considered.  x = h(...args)
y = g(x) = g(h(...args))
z = f(y) = f(g(x)) = f(g(h(...args)))
To obtain z value in combination of f, g, and h, with (...args), compose is applied to represent f(y) more easily.compose(f, g, h) is as follows:
(...args) => f(g(h(...args)))

compose is available to apply an enhancer with many compositions to createStore , as the following example shows.

// https://redux.js.org/api-reference/composeimport { createStore, applyMiddleware, compose } from 'redux'
import thunk from 'redux-thunk'
import DevTools from './containers/DevTools'
import reducer from '../reducers'
const store = createStore(
reducer,
compose(
applyMiddleware(thunk),
DevTools.instrument()
)
)

The compose function is available within Redux for many middleware. Now, let’s find out what Middleware is really about.

Middleware

Redux middleware, in the dispatching process, allows to add tasks before an action reaches a reducer.

Principle

Let’s assume preceding and following processes are required for an action verification, filtering, monitoring, interfacing with external APIs, and asynchronous processing. Such tasks must be processed in the action creator, or by monkey-patching the dispatch function. However, you may encounter many problems with maintenance issues, including code duplication, complexity, and non-pure functions, and that is where middleware comes in. Before an action arrives at reducer, middleware receives it and perform tasks mentioned previously so as to resolve problems. In the end, middleware creates a powerful dispatch function.

That is why Redux tells a base dispatch function without middleware, from a higher-order dispatching function. Such function can be implemented by using compose described earlier. From now on, this document will tell the difference between baseDispatch and dispatch .

  1. Middleware gets {getState, dispatch}, the Store API, as parameter.
  2. And returns the new wrapDispatch function.
  3. The wrapDispatch function gets a chained function called next as parameter.
  4. Finally, an action is delivered to a reducer via the next function.

In short, middleware is implemented into three composed functions.

function middleware({getState, dispatch}) {
return function wrapDispatch(next) {
return function dispatchToSomething(action) {
// ...
return next(action);
}
}
}

With arrow-syntax of ES6, they are easier to see.

const middleware = ({getState, dispatch}) => next => action => {
// ...
return next(action);
}

The next function is required to perform the ongoing dispatch process that does not return to the beginning from current chaining. The function is needed to get into a dispatching function of the next middleware, when many middleware are composed in a store.

Let’s move on to the applyMiddleware API. For your information, applyMiddleware is a higher-order function that covers createStore .

The interesting part is the middlewareAPI, which wraps the dispatch function once again. What if it would be represented simply as below?

const middlewareAPI = {
getState: store.getState,
dispatch: store.dispatch
};

The middleware always looks ahead to baseDispatch. However, in the case of asynchronous action/middleware, actions need to go back to middleware chain at the beginning. That is, sometimes, store.dispatch(action) is required in place of next(action), and in such cases, dispatch must be considered as a closure variable where the whole middleware is connected(= dispatch = compose(…chain)(store.dispatch)).

If you’re still not sure, check differences between the two codes below.

let foo = () => console.log('foo')const a = {foo}
const b = {
foo: () => foo()
}
foo = () => console.log('new foo')a.foo() // foo
b.foo() // new foo

Refer to redux-thunk library for a simple example of middleware. When an action is a function, not a plain object, the function can be executed to operate asynchronous logic or an external API.

Reducers

As the name Redux, a combination of Reducer and Flux, suggests, reducer is the key concept and we should learn its details.

So far, we have learned how an action is dispatched and goes through middleware. An action from outside is delivered to a reducer along with the store state, and the store gets a new state from reducer. That is why a reducer is defined as a pure function that receives the previous state and action to return a new state.

reducer: (previousState, action) => newState

Significance

For any application, it is very difficult to compose a whole state with a single reducer. The state of an application may have tens or hundreds of values, depending on cases. It is almost impossible to manage all these values under a reducer. If it had ever been possible to design an application with a reducer, we wouldn’t have named it reducer. A reducer can be composed of a combination of unit reducers for a unit state. Application developers can define reducers at lower units and apply combineReducers API at the end to combine many reducers, so that a big root of reducers can be composed.

combineReducers can be simply describe as below:

As each reducer is used as a callback delivered from combineReducers to the Array.prototype.reduce API, we use the term, reducer (although Array.prototype.reduce is not used for an actual implementation any more).

You can find from the codes the combineReducers has only one depth to deal with. However, combineReducers can be implemented in n-depth state, if it is recursively combined. Therefore, application developers can write unit reducers that process each unit state only, no matter the depth, and combine them to create a root reducer.

State Change for Action

As was simply implemented, combinedReducers always returns a new object, irrespective of stage changes. But, if an event listener is performed and no changes were actually incurred, how would an application developer find out? There is no other way than comparing each property one by one. Therefore, a reducer must always return the previous state with the same reference, when there is no change. Let’s look at a part of the actual implementation code of the combineReducers API.

The reducer returned at combineReducers also perform hasChanged = hasChanged || nextStateForKey !== previousStateForKey for all state properties, and determines either a new state or a previous state to be returned, depending on the hasChanged. That is why application developers must follow the rule, by which only when there is a state change in a unit reducer, return a object with new references; otherwise, return the previous.

Conclusion

Redux, as a quite simple library, contains very important concept and uses sophisticated technological methods. By analyzing Redux, we have learned a great deal about how to use and combine the flow of unidirectional data, compose, and reducers. In particular, implementing reducers as pure functions and combining them as a state tree is something anyone would think about but hard to realize. I recommend that you read through the Redux codes, which are only 2KB but full of insights.

Reference

--

--

TOAST UI
TOAST UI

Responses (1)