Abstract

In the React world, there are hundreds of state management solutions, but one of the best is Redux. If you want to learn functional programming, then Redux source code is the best learning material. Considering that many people use Vue, I try to make this article easy to understand, so that students who have not been exposed to the React technology stack can also master Redux.

In the React world, there are hundreds of state management solutions, but one of the best is Redux. If you want to learn functional programming, then Redux source code is the best learning material. Considering that many people use Vue, I try to make this article easy to understand, so that students who have not been exposed to the React technology stack can also master Redux.

In the React world, there are hundreds of state management solutions, but one of the best is Redux. If you want to learn functional programming, then Redux source code is the best learning material. Considering that many people use Vue, I try to make this article easy to understand, so that students who have not been exposed to the React technology stack can also master Redux.

Redux is a typical “a hundred lines of code, a thousand lines of documentation”, with very little core code, but not a simple idea, which can be summed up in the following two points:

  • Global state is unique and Immutable, and Immutable means that when a state is modified, it is replaced by a new one rather than by making changes directly to the original data:
 let store = { foo: 1.bar: 2 };

 // When a status update is required
 // Create a new object and replace itstore = { ... store,foo: 111 };
Copy the code

This is in contrast to Vue, where changes must be made directly on the original object in order to be listened on by a reactive mechanism to trigger setter notifications for dependency updates.

Status updates are done using a pure function (Reducer). Pure functions have the following characteristics:

  • Outputs relate only to inputs;
  • References are transparent, independent of external variables;
  • No side effects;

So for a pure function, the same input must produce the same output, very stable. Global state is modified using pure functions so that the global state can be predicted.

1. A few concepts to know

Here are some concepts to understand before using Redux and reading the source code:

Action

Action is a plain JavaScript object that describes how to modify the state, and it needs to include the Type attribute. A typical action would look like this:

const addTodoAction = {
  type: 'todos/todoAdded'.payload: 'Buy milk'
}
Copy the code

Reducers

Reducer is a pure function whose function signature is as follows:

/ * * *@param {State} State Current status *@param {Action} Action Describes how to update status *@returns Updated status */
function reducer(state: State, action: Action) :State
Copy the code

The reducer functions take their name from the array reduce method because they resemble the callback functions passed by the array Reduce method, in that the last value returned is passed in as an argument to the next call.

The compile of the Reducer function should strictly follow the following rules:

  • Check whether the Reducer cares about the current action
  • If so, a copy of the state is created, the state in the copy is updated with the new value, and the copy is returned
  • Otherwise, the current state is returned

A typical Reducer function looks like this:

const initialState = { value: 0 }

function counterReducer(state = initialState, action) {
  if (action.type === 'counter/incremented') {
    return {
      ...state,
      value: state.value + 1}}return state
}
Copy the code

Store

The current state of the Redux application instance created by calling createStore can be obtained using the getState() method.

Dispatch

Method of store instance exposure. The only way to update the status is to submit an action through Dispatch. The store will call Reducer to perform the state update, and the updated state can be obtained using the getState() method:

store.dispatch({ type: 'counter/incremented' })

console.log(store.getState())
// {value: 1}
Copy the code

storeEnhancer

CreateStore’s higher-order function package is used to enhance the store’s capabilities. Redux’s applyMiddleware is an enhancer officially available.

middleware

A higher-order function wrapper to Dispatch, with applyMiddleware replacing the original Dispatch with an implementation containing the middleware chain calls. Redux-thunk is the official middleware that supports asynchronous actions.

2. Basic usage

Before we look at the source code, let’s take a look at the basics of Redux to get a better understanding of the source code.

First, we wrote a Reducer function as follows:

// reducer.js
const initState = {
  userInfo: null.isLoading: false
};

export default function reducer(state = initState, action) {
  switch (action.type) {
    case 'FETCH_USER_SUCCEEDED':
      return {
        ...state,
        userInfo: action.payload,
        isLoading: false
      };
    case 'FETCH_USER_INFO':
      return { ...state, isLoading: true };
    default:
      returnstate; }}Copy the code

In the code above:

  • InitState is passed as the initial state when reducer is first called, and then switch… The default at the end of case is used to get the initial state
  • In the switch… Case also defines two action. types that specify how to update the status

Next we create a store:

// index.js
import { createStore } from "redux";
import reducer from "./reducer";

const store = createStore(reducer);
Copy the code

The store instance exposes two methods, getState for retrieving state, Dispatch for submitting action modification state, and subscribe for store changes:

// index.js

// Subscribe to store changes after each status update
store.subscribe(() = > console.log(store.getState()));

// Get the initial state
store.getState();

// Submit action to update the status
store.dispatch({ type: "FETCH_USER_INFO" });
store.dispatch({ type: "FETCH_USER_SUCCEEDED".payload: "Test content" });
Copy the code

If we run the above code, the console will print one after another:

{ userInfo: null.isLoading: false } // Initial state
{ userInfo: null.isLoading: true } // First update
{ userInfo: "Test content".isLoading: false } // Second update
Copy the code

3. Redux Core source code analysis

The above example is simple, but it already contains the core functionality of Redux. Let’s take a look at how the source code is implemented.

createStore

All the core ideas of Redux’s design are in createStore. CreateStore is a very simple implementation, consisting of a closed environment that caches currentReducer and currentState, and defines methods such as getState, Subscribe, and Dispatch.

The core source code for createStore is as follows: Else logic is omitted, and the type annotations in the source code are also removed for easy reading:

// src/createStore.ts
function createStore(reducer, preloadState = undefined) {
  let currentReducer = reducer;
  let currentState = preloadState;
  let listeners = [];

  const getState = () = > {
    return currentState;
  }

  const subscribe = (listener) = > {
    listeners.push(listener);
  }

  const dispatch = (action) = > {
    currentState = currentReducer(currentState, action);

    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i];
      listener();
    }

    return action;
  }
  
  dispatch({ type: "INIT" });

  return {
    getState,
    subscribe,
    dispatch
  }
}
Copy the code

The createStore call link is as follows:

  • The createStore method is called first, passing in reducer and preloadState. PreloadState represents the initial state. If not, reducer must specify the initial value.
  • Reducer and preloadState are assigned to currentReducer and currentState, respectively, to create closures.
  • Create an array of Listeners based on the publish-subscribe model, which is the event center of the publish-subscribe model and is also cached by closure.
  • Create functions like getState, SUBSCRIBE, dispatch, etc.
  • Call the dispatch function and submit an INIT action to generate the initial state. In the Redux source code, type is a random number;
  • Finally, return an object containing getState, SUBSCRIBE, and Dispatch functions, i.e. store instance.

So obviously, the value of the closure is not accessible to the outside world, only through the getState function.

To subscribe to status updates, you can use the SUBSCRIBE function to push a listener to the event center (note that the listener allows side effects).

Call Dispatch to submit the action when you need to update the status. Call currentReducer (the Reducer function) in the Dispatch function, pass in currentState and Action, and generate a new state, which is passed to currentState. Once the status update is complete, the subscribed listener is executed once (in fact, just calling Dispatch will trigger the listener even if no changes are made to the state).

If you’re familiar with object-oriented programming, you might say that createStore does things that can be encapsulated in a class. Yes, I use TypeScript to implement the following:

type State = Object;
type Action = {
  type: string; payload? :Object;
}
type Reducer = (state: State, action: Action) = > State;

// Define the IRedux interface
interface IRedux {
  getState(): State;
  dispatch(action: Action): Action;
}

// Implement the IRedux interface
class Redux implements IRedux {
  // Set the member variable to private
  // it is equivalent to closureprivate currentReducer: Reducer; private currentState? : State;constructor(reducer: Reducer, preloadState? : State) {
    this.currentReducer = reducer;
    this.currentState = preloadState;
    this.dispatch({ type: "INIT" });
  }
  
  public getState(): State {
    return this.currentState;
  }

  public dispatch(action: Action): Action {
    this.currentState = this.currentReducer(
      this.currentState,
      action
    );
    returnaction; }}// Create an instance from factory mode
function createStore(reducer: Reducer, preloadState? : State) {
  return new Redux(reducer, preloadState);
}
Copy the code

You see, it’s interesting that functional programming and object-oriented programming come together.

applyMiddleware

ApplyMiddleware is one of Redux’s toughest points. There isn’t much code, but it uses a lot of functional programming tricks that I didn’t fully understand until I did a lot of source code debugging.

The first thing is to be able to read it:

const middleware =
  (store) = >
    (next) = >
      (action) = > {
        // ...
      }
Copy the code

The above is equivalent to:

const middleware = function(store) {
  return function(next) {
    return function(action) {
      // ...}}}Copy the code

The second thing you need to know is that this is really just a function currization, which is that you can take parameters step by step. If the inner function has a variable reference, then each call generates a closure.

When you think of closures, some of you immediately think of memory leaks. In fact, closures are very common in everyday project development, and many times we create them by accident, but often we ignore them.

The main purpose of closures is to cache values, which is similar to the effect of declaring a variable in an assignment. The difficulty with closures is that variables are explicitly declared, whereas closures tend to be implicit, and it’s easy to ignore when a closure is created and when its value is updated.

Functional programming is all about closures, so to speak. You’ll see plenty of examples of closures in the source code analysis below.

ApplyMiddleware is Redux’s storeEnhancer implementation, which implements a plugin mechanism that increases store capabilities, such as asynchronous actions, Logger log printing, state persistence, and more.

export default function applyMiddleware<Ext.S = any> (. middlewares: Middleware
       
        []
       ,>) :StoreEnhancer<{ dispatch: Ext }>
Copy the code

In my opinion, the advantage of this is that it provides room to build wheels

ApplyMiddleware accepts one or more instances of middleware and passes them to createStore:

import { applyMiddleware, createStore } from "redux";
import thunk from "redux-thunk"; // Use thunk middleware
import reducer from "./reducer";

const store = createStore(reducer, applyMiddleware(thunk));
Copy the code

The createStore entry accepts only one storeEnhancer, and if you need to pass in more than one, you can combine them using the compose function in Redux Utils.

The compose function is covered later

From the usage above, you can assume that applyMiddleware is also a higher-order function. CreateStore has some if.. Else logic is omitted because storeEnhancer is not used. So let’s take a look over here.

First look at createStore’s function signature, which actually accepts 1-3 parameters. Reducer must be passed. When the second argument is a function type, storeEnhancer is identified. If the second argument is not of a function type, it is recognized as preloadedState, in which case we can also pass storeEnhancer of a function type:

function createStore(reducer: Reducer, preloadedState? : PreloadedState | StoreEnhancer, enhancer? : StoreEnhancer) :Store
Copy the code

You can see the logic of parameter verification in the source code:

// src/createStore.ts:71
if((typeof preloadedState === 'function' && typeof enhancer === 'function') | | (typeof enhancer === 'function' && typeof arguments[3= = ='function')) {Throw an exception when passing two function type arguments
  // Only one storeEnhancer is accepted
  throw new Error(a); }Copy the code

When the second argument is a function type, treat it as storeEhancer:

// src/createStore.ts:82
if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
  enhancer = preloadedState as StoreEnhancer<Ext, StateExt>
  preloadedState = undefined
}
Copy the code

Here’s the hard logic:

// src/createStore.ts:87
if (typeofenhancer ! = ='undefined') {
  // If enhancer is used
  if (typeofenhancer ! = ='function') {
    // Raise an exception if enhancer is not a function
    throw new Error(a); }// Returns the result of the enhancer call directly, without further store creation
  // enhancer is definitely a higher-order function
  Reducer preloadedState reducer preloadedState
  CreateStore is most likely to be called again within enhancer
  return enhancer(createStore)(
    reducer,
    preloadedState
  )
}
Copy the code

Let’s take a look at the source code for applyMiddleware, with the type annotations removed for ease of reading:

// src/applyMiddleware.ts
import compose from './compose';

function applyMiddleware(. middlewares) {
  return (createStore) = > (reducer, preloadedState) = > {
    const store = createStore(reducer, preloadedState);
    let dispatch = () = > {
      throw new Error(a); }const middlewareAPI = {
      getState: store.getState,
      dispatch: (action, ... args) = >dispatch(action, ... args) }const chain = middlewares.map(middleware= >middleware(middlewareAPI)); dispatch = compose(... chain)(store.dispatch);return {
      ...store,
      dispatch
    }
  }
}
Copy the code

As you can see, there is not much code here, but there is a case where a function is nested within a function:

const applyMiddleware = (. middlewares) = >
  (createStore) = >
    (reducer, preloadedState) = > {
      // ...
    }
Copy the code

Analyze the call link in the source code:

  • When applyMiddleware is called, the middleware instance is passed in and enhancer is returned. The usage of the remaining arguments supports passing in more than one middleware;
  • Enhancer was called by createStore, and reducer and preloadedState were passed in two times.
  • CreateStore is called internally again. This time, because enhancer is not passed, the store is created directly.
  • Create a decorated Dispatch method that overrides the default dispatch;
  • Construct middlewareAPI and inject middlewareAPI into middleware.
  • Combine middleware instances into a function and pass the default store.dispatch method to the middleware;
  • Finally, a new store instance is returned, with the Store Dispatch method embellished with middleware;

The compose function, which is often used in the functional programming paradigm, creates a right-to-left stream of data, passes the results of the right-side function’s execution as arguments to the left, and returns a function that executes in the stream above:

// src/compose.ts:46
export default function compose(. funcs) {
  if (funcs.length === 0) {
    return (arg) = > arg
  }
  if (funcs.length === 1) {
    return funcs[0]}return funcs.reduce(
    (a, b) = >
      (. args) = >a(b(... args)) ) }Copy the code

Question to consider: If you want to change the execution order from left to right, what should you do?

From the code here, we can easily deduce the structure of a middleware:

function middleware({ dispatch, getState }) {
  / / receive middlewareAPI
  return function(next) {
    // Accept the default store.dispatch method
    return function(action) {
      // The receiving component invokes the action passed in by Dispatch
      console.info('dispatching', action);
      let result = next(action);
      console.log('next state', store.getState());
      returnresult; }}}Copy the code

Looking at this, I think most readers have two questions:

  1. Is the Dispatch function obtained through the middlewareAPI and the dispatch function eventually exposed by the Store instance embellished?
  2. To prevent dispatch from being called when middleware is created, applyMiddleware initializes the new Dispatch as an empty function that throws an exception.

You can try to think about it a little bit.

To tell the truth, I have been troubled by these two problems when reading the source code, and most technical articles have not been explained. There is no way, but through debugging source code to find the answer. After much debugging, it becomes clear that the middlewareAPI’s Dispatch function itself is introduced in the form of a closure that few people might recognize:

// Define a new dispatch method
// This is an empty function that throws an exception
let dispatch = () = > {
  throw new Error(a); }/ / define middlewareAPI
// Note that dispatches are introduced in the form of closures
const middlewareAPI = {
  getState: store.getState,
  dispatch: (action, ... args) = >dispatch(action, ... args) }// Inject middlewareAPI into middleware
// Calling Dispatch in Middleware at this point throws an exception
const chain = middlewares.map(middleware= > middleware(middlewareAPI));
Copy the code

The following code does two things: it combines middleware into a function that injects the default Dispatch function, and it replaces the empty function that started with the new Dispatch with a normal executable function. Since the middlewareAPI’s Dispatch is introduced as a closure, when the Dispatch is updated, the value in the closure is updated accordingly:

// Replace dispatch with the normal dispatch method
// Note that the values in the closure are updated accordingly, and middleware can access the updated methodsdispatch = compose(... chain)(store.dispatch);Copy the code

That is, the dispatches exposed by createStore and middleware get decorated dispatches that look like this:

function(action) {
  // Notice that there is a closure
  // We can get the dispatch, getState, and next passed in for middleware initialization
  // If you interrupt the point, you can see the variables of the closure in scope
  // Also note that dispatch is the function itself
  console.info('dispatching', action);
  let result = next(action);
  console.log('next state', store.getState());
  return result;
}
Copy the code

4. Handle asynchronous actions

Because reducer needs to be strictly controlled as a pure function, asynchronous operations and network requests cannot be carried out in reducer. The dispatch function can be called from an asynchronous callback even though asynchronous code cannot be placed in the reducer:

setTimeout(() = > {
  this.props.dispatch({ type: 'HIDE_NOTIFICATION'})},5000)
Copy the code

In the React component, connect is used to map dispatch to the props of the component, similar to mapAction in Vuex.

You can! Redux author Dan Abramov has a very good answer on Stackoverflow that approves of this usage:

Stackoverflow.com/questions/3…

I summarize Dan Abramov’s core ideas below.

  • Redux does provide some alternatives for handling asynchronous actions, but should only be used if you realize you’re writing a lot of template code. Otherwise use the simplest solution (don’t add entities if not necessary);
  • When multiple components need to use the same action.type, to avoid misspelling of action.type, separate the common actionCreator, for example:
 // actionCreator.js
 export function showNotification(text) {
   return { type: 'SHOW_NOTIFICATION', text }
 }
 export function hideNotification() {
   return { type: 'HIDE_NOTIFICATION'}}// component.js
 import { showNotification, hideNotification } from '.. /actionCreator'

 this.props.dispatch(showNotification('You just logged in.'))
 setTimeout(() = > {
   this.props.dispatch(hideNotification())
 }, 5000)
Copy the code
  • The above logic works perfectly in simple scenarios, but several problems arise as business complexity increases:
  1. Typically, status updates take several steps, and there is a logical sequence, such as show and hide notifications, resulting in more template code;
  2. The submitted action has no state. If a race condition occurs, the status update may cause a bug.
  • For the above problems, it is necessary to extract the actionCreator of separate steps to encapsulate the operations involving status update for easy reuse and generate a unique id for each dispatch:
 // actions.js
 function showNotification(id, text) {
   return { type: 'SHOW_NOTIFICATION', id, text }
 }
 function hideNotification(id) {
   return { type: 'HIDE_NOTIFICATION', id }
 }

 let nextNotificationId = 0
 export function showNotificationWithTimeout(dispatch, text) {
   const id = nextNotificationId++
   dispatch(showNotification(id, text))

   setTimeout(() = > {
     dispatch(hideNotification(id))
   }, 5000)}Copy the code

Then use this in the page component to resolve the template code and status update conflicts:

// component.js
showNotificationWithTimeout(this.props.dispatch, 'You just logged in.')

// otherComponent.js
showNotificationWithTimeout(this.props.dispatch, 'You just logged out.')
Copy the code
  • If you’re careful, you’ll notice that you pass the dispatch. This is because normally only components can access Dispatch, and in order to make it accessible to externally wrapped functions, we need to pass Dispatch as a parameter;
  • If store is a global singleton, you can access it directly:
 // store.js
 export default createStore(reducer)

 // actions.js
 import store from './store'

 // ...

 let nextNotificationId = 0
 export function showNotificationWithTimeout(text) {
   const id = nextNotificationId++
   store.dispatch(showNotification(id, text))

   setTimeout(() = > {
     store.dispatch(hideNotification(id))
   }, 5000)}// component.js
 showNotificationWithTimeout('You just logged in.')

 // otherComponent.js
 showNotificationWithTimeout('You just logged out.')
Copy the code
  • This is operationally possible, but the Redux team doesn’t believe in singletons. Their reasoning is that if the store becomes a singleton, it will make server-side rendering difficult to implement and testing inconvenient, such as changing all import to mock Store;
  • For these reasons, the Redux team recommends passing the Dispatch with a function argument, even though it’s cumbersome. So is there a solution? Yes, redux-thunk solves this problem;
  • In fact, redux-Thunk teaches Redux to recognize special actions of function types;
  • After the middleware is enabled, when the Action of dispatch is a function type, redux-thunk will pass dispatch to this function as a parameter. It should be noted that the reducer still received ordinary JavaScript objects as actions:
 // actions.js

 function showNotification(id, text) {
   return { type: 'SHOW_NOTIFICATION', id, text }
 }
 function hideNotification(id) {
   return { type: 'HIDE_NOTIFICATION', id }
 }

 let nextNotificationId = 0
 export function showNotificationWithTimeout(text) {
   return function (dispatch) {
     const id = nextNotificationId++
     dispatch(showNotification(id, text))

     setTimeout(() = > {
       dispatch(hideNotification(id))
     }, 5000)}}Copy the code

Use the following in the component:

// component.js
this.props.dispatch(showNotificationWithTimeout('You just logged in.'))
Copy the code

Well, that concludes Dan Abramov’s point.

Redux-thunk does not provide an asynchronous solution by itself. The simplest way to do this is to put the dispatch function in an asynchronous callback. Most of the time, we will encapsulate asynchronous actionCreator. In asynchronous operations, it is troublesome to pass dispatch every time. Redux-thunk encapsulates the dispatch function at a higher level, allowing it to accept function-type actions. Pass both Dispatch and getState to the Action so that it doesn’t have to be passed manually each time.

Before you look at the source code, consider the internal implementation of Redux-Thunk with the applyMiddleware source code.

The redux-Thunk implementation principle is very simple, the code is as follows:

// src/index.ts:15
function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) = >
    next= >
      action= > {
        if (typeof action === 'function') {
          return action(dispatch, getState, extraArgument)
        }

        return next(action)
      }
}
Copy the code

Inside redux-Thunk, the createThunkMiddleware method is first called to get a higher-order function and then exported. This function is the middleware structure we analyzed earlier:

({ dispatch, getState }) =>
  next= >
    action= > {
      if (typeof action === 'function') {
        return action(dispatch, getState, extraArgument)
      }

      return next(action)
    }
Copy the code

During initialization, applyMiddleware injects middlewareAPI (corresponding to the Dispatch and getState parameters) and Store. dispatch (the original Dispatch method) into Thunk, Corresponding to the next parameter).

After initialization, the Store instance’s Dispatch is replaced with a decorated Dispatch method (the Dispatch in the middlewareAPI is also replaced because it is a closure reference), Dispatch.tostring () prints the following:

// Notice that dispatch, getState, and Next are accessible in closures
// The initialized dispatch is actually the function itself
action => {
  if (typeof action === 'function') {
    return action(dispatch, getState, extraArgument)
  }

  return next(action)
}
Copy the code

The next thing is easy when we submit an Action of type function:

// actions.js
const setUserInfo = data= > ({
  type: "SET_USER_INFO".payload: data
})

export const getUserInfoAction = userId= > {
  return dispatch= > {
    getUserInfo(userId)
      .then(res= >{ dispatch(setUserInfo(res)); }}})// component.js
import { getUserInfoAction } from "./actionCreator";

this.props.dispatch(getUserInfoAction("666"));
Copy the code

Call this function when the submitted action is of function type, and pass in the dispatch, getState, and extraArgument arguments:

if (typeof action === 'function') {
  return action(dispatch, getState, extraArgument)
}
Copy the code

(As you can see here, in addition to Dispatch, getState and extraArgument can be accessed inside the Action of the function type.)

When the asynchronous operation is complete, calling the Dispatch method passed by Redux-thunk to submit the Action object type, we still enter the same decorated Dispatch method, but enter another branch when determining the type:

return next(action);
Copy the code

Here, next is the original Dispatch method of Redux, which submits the actions of the object type to the Reducer method and finally performs the status update.

5. To summarize

Redux is a very classic state management solution. It follows the principles of functional programming, where state is read-only and immutable, and can only be updated through pure functions.

But Redux also has its problems. First of all, for beginners, it is expensive to get started, so it is necessary to understand the concept and design of functional programming before using it. Second, Redux is very cumbersome in actual development, even if the implementation of a very simple function, may need to modify 4-5 files at the same time, reducing development efficiency. Vuex, by contrast, is cheap to get started, friendly to beginners, and easy to use, requiring neither asynchronous middleware nor additional UI binding. All functionality provided by plug-ins in Redux is built right out of the box.

Redux provides an official encapsulation solution, Redux Toolkit, and the community also provides many encapsulation solutions, such as Dva and Rematch, to simplify the use of Redux. In terms of API encapsulation, Vuex is used as a reference. There is even a Mobx state management scheme that is Vue responsive and uses Mutable data. The React team also recently launched the Recoil State Manager library.

reference

redux.js.org

Github.com/reduxjs/red…

Github.com/reduxjs/red…

Stackoverflow.com/questions/3…

Elegant guide to coding: Functional programming