In the past two years, the development of front-end technology has been in full swing, and a large number of front-end projects have been used or turned to the Vue and React teams. Single-page applications rendered by the front-end account for an increasingly high proportion, which means that the complexity of front-end work is also rising sharply, and the information displayed on the front-end page is more and more complex. We know that any state needs to be managed, so today we’re going to talk about front-end state management.

Virtual DOM and React were born

AngularJS plays an important role in Web application development. However, the bidirectional binding of AngularJS data and views, based on dirty detection, suffers from a performance disadvantage, as any data changes redraw the entire view. However, the idea of automatically updating pages based on statuses is advanced. In order to solve performance problems, Facebook engineers proposed the idea of Virtual DOM. When the DOM is put into memory and the state changes, a new Virtual DOM is generated according to the state, and then it is compared with the previous Virtual DOM through a diff algorithm. The changed content is rendered in the browser. Avoid JS engine frequently call DOM operation interface of rendering engine, make full use of THE performance of JS engine. With Virtual DOM support, React was born.

React makes the idea of “state => view” a good practice. However, on the contrary, how to properly modify state in view becomes a new problem. For this reason, Facebook proposed Flux idea.

Flux thought

Yes, Flux is not the name of a JS library, but an architectural idea. Many JS libraries are the realization of this idea, such as Alt and Fluxible. It is used to build client Web applications and regulate the flow of data in Web applications.

So what does this have to do with state management? We know, the React just a view layer library, did not have any restrictions on data layer, in other words are likely to have to change in any view component data layer code, and decentralization for a data layer management is bad, the other problems will be difficult to trace once the data layer, because don’t know change from which components. In addition, if the data is passed from the parent component to the child component via props, there will be coupling between the components, which violates the principle of modularity.

Take an AngularJS application as an example. In AngularJS, a Controller is a closure contained within the scope $scope. This closure corresponds to a view template into which data from $Scope will be rendered. However, a template may correspond to multiple models ($scope of the current controller, parent $scope, Isolated Scope of instructions, etc.), and a model may also affect the rendering of multiple templates. Once the scale of the application becomes larger, the relationship between data and view is easily confused. As data and view interact with each other in this process, the burden of thinking also increases.

However, Flux has a one-way way of thinking. It collects the controller code of the modified data layer previously delegated to each component and manages it in a unified manner. If the component needs to modify the data layer, it needs to trigger a specific pre-defined Dispatcher. The Dispatcher then applies the action to the Model to make changes to the data layer. Changes to the data layer are then applied to the view, creating a one-way data flow. For example, this is like the management of the library. It was open in the past, and all people could enter and exit the library to borrow and return books at will. If the number of people is small, this method can reduce the process and increase efficiency, but once the number of people increases, it is bound to cause chaos. Flux is like adding an administrator to the library. All borrowing and returning behaviors need to be entrusted to the administrator. The administrator will standardize the operation behavior of the library and record everyone’s operation to reduce the confusion.

Main Flux implementation

There are many Flux implementations, and different implementations have their own highlights. Here are some of the more popular Flux implementations.

Flux

This should be a more official “implementation of Flux”, which appears to be well-behaved and implements the basic concepts in the Flux architecture documentation. The core of Dispatcher is the Dispatcher. With Dispatcher, users can register corresponding action types, register corresponding callbacks for different actions, trigger actions, and transmit payload data.

Here is a simple example:

const dispatcher = new Dispatcher()
const store = {books: []}
​
dispatcher.register((payload) => {
  if (payload.actionType === 'add-book') {
    store.books.push(payload.newBook)
  }
})
dispatcher.dispatch({
  actionType: 'add-book',
  newBook: {
    name: 'cookbook'
  }
})
Copy the code

As you can see, it is possible to use only the Dispatcher provided by Flux, but it is recommended to use some of the base classes provided by Flux to build stores. These base classes provide methods that can be called to better extend the functionality of the data layer. For details, see the Flux documentation.

Reflux

The Reflux is a Flux implementation written on the basis of Flux, which formally removes the explicit Dispatcher, presents the action as a function, and constructs an action in the following manner:

const addBook = Reflux.createAction({ actionName: 'add-book', sync: false, preEmit: function() {/*... * /}, / /... }) addBook({/*... * /})Copy the code

In addition, Reflux has some differences compared to Flux, for example:

Rely on

Flux is not a library, but an architectural idea, although using Flux still requires the introduction of a Dispatcher, whereas Reflux provides a full set of libraries for you to use and can be easily installed via NPM.

Component listening event

There are also some differences between Flux and Reflux in the writing of in-component listening events, in Flux:

const _books = {}
const BookStore = assign({}, EventEmitter.prototype, {
  emitChange () {
    this.emit(CHANGE_EVENT)
  },
  addChangeListener (callback) {
    this.on(CHANGE_EVENT, callback)
  },
  removeChangeListener (callback) {
    this.removeListener(CHANGE_EVENT, callback)
  }
})
const Book = React.createClass({
  componentDidMount:function(){
    bookStore.addChangeListener(this.onAddBook)
  }
})
Copy the code

In the Reflux, which is written somewhat differently, the listenTo method is invoked in the component by introducing mixins in the component:

var BookStore = React.createClass({
  mixins: [Reflux.ListenerMixin],
  componentDidMount: function() {
    this.listenTo(bookStore, this.onAddBook)
  }
})
Copy the code

Store and Action

In Flux, it is troublesome to initialize a Store and write an Action, which leads to an increase in the amount of code and a decrease in maintainability. For example, we still need to write a Store and the corresponding Action. The way to create a Store is already shown in the example above. Creating an Action is different from creating an Action.

const fluxActions = {
  addBook: function(book) {
    Dispatcher.handleViewAction({
      actionType: 'ADD_BOOK',
      book
    })
  },
  // more actions
}
Copy the code

The Reflux is much simpler than Flux:

const refluxActions = Reflux.createActions([
  'addBook',
  // more actions
])
Copy the code

The reason Reflux is so much simpler is that it can directly register callback functions for events in the Store, removing the middle layer of Dispatcher, or simply integrating the functionality of the Dispatcher into the Store.

In general, Reflux is pretty much an improved version of Flux, completing the functionality that Flux lacks on Store, removing the Dispatcher (not actually removed, but incorporated with Store) and eliminating redundant code.

Redux

Redux is the equivalent of Reduce + Flux. Like Flux, Redux requires you to maintain a data layer to represent the state of the application. The difference is that Redux does not allow you to modify the data layer, but only allows you to describe changes through an Action object. In Redux, the Dispatcher is removed and replaced with a pure function that takes the original state Tree and action as arguments and generates a new state tree in its place. This so-called pure function is the important concept in Redux — Reducer.

In functional programming, Reduce operation means to obtain the final product by iterating through the elements of a set and substituting the results of the previous operation into the next operation. In Redux, Reducer reflects this process by combining old state and action and obtaining a new state.

Therefore, the second difference between Redux and Flux is that Redux does not modify any states, but replaces the old ones with newly generated states. This is Immutable Data. Modifying state directly from a Reducer is not allowed. Facebook’s Immutable library helps you to use Immutable Data. For example, build a Store that can be used in Redux.

Here is an example of building an application’s state management using Redux:

const { List } = require('immutable')
const initialState = {
  books: List([])
}
import { createStore } from 'redux'
​
// action
const addBook = (book) => {
  return {
    type: ADD_BOOK,
    book
  }
}
​
// reducer
const books = (state = initialState, action) => {
  switch (action.type) {
    case ADD_BOOK:
      return Object.assign({}, state, {
        books: state.books.push(action.book)
      })
  }
  return state
}
​
// store
const bookStore = createStore(books, initialState)
​
// dispatching action
store.dispatch(addBook({/* new book */}))
Copy the code

The way Redux works follows the strict principle of one-way data flow, and as you can see from the code example above, the entire lifecycle is divided into:

  1. Call Dispatch in the store and pass in the Action object. The Action object is a generic object that describes changes, which in the example is generated by a Creator function.

  2. Next, the Store calls the Reducer function that was passed in when the Store was registered, and the current state and action are passed in as parameters. In the Reducer, the new state is calculated and returned.

  3. Store saves the new state tree generated by reducer, and then use the new state to generate a new view. This step can be done with the help of some libraries, such as the official recommended React Redux.

If an application is large, the reducer may be too large. At this time, we can split the reducer. For example, combineReducers is used to pass multiple reducer as parameters and generate a new reducer. When an action is triggered, the new Reducer triggers multiple existing reducers:

const book(state = [], action) => {
  // ...
  return newState
}
const author(state = {}, action) => {
  // ...
  return newState
}
const reducer = combineReducers({ book, author })
Copy the code

For more uses of Redux, you can read the documentation carefully, which is not covered here.

There are more state management libraries available on the React stack, such as Relay, but it needs to work with GraphQL, which is hard to introduce without GraphQL support, so I won’t go into details here (I haven’t actually studied 😝).

Time back to tell them

React flux-like state management tools will be available in the React stack in the next part of this article