Data flow for the front-end framework

The front-end framework implements data-driven view changes, we describe the binding relationship between data and view using Template or JSX, and then only care about data management. Data is passed from component to component and from component to global store, called the data flow of the front-end framework.

Generally speaking, business data and state data that multiple components care about are stored in the Store, except that some state data is only cared by a component and we put state data in the component. The component takes data from the store and notifies the store to change the corresponding data when interacting with it.

The store doesn’t have to be redux or Mobox. The react context can also be used as a store. The problem with context as a store, however, is that any component can retrieve data from the context and modify it. This makes it particularly difficult to detect problems because it is not known which component corrupted the data, i.e. the data flow is not clear.

For this reason, we rarely see a context as a store, but it’s always with a redux.

So why is Redux good? The first reason is that the data flow is clear and there is a unified entry point for changing the data.

In each component, an action is dispatched to trigger the modification of store, and the modification logic is all in the Reducer. The component then listens to the data changes of store and extracts the latest data from the reducer.

The data flow is one-way, clear and easy to manage.

It’s the same reason why we have to go through the approval stream to get access to anything we want in the company, rather than just going to someone. The centralized management process is clear and traceable.

Management of asynchronous processes

In many cases changing store data is an asynchronous process, such as waiting for a network request to return data, periodically changing data, waiting for an event to change data, etc. Where is the code for these asynchronous processes?

Components?

It’s ok to put it in components, but how can asynchronous processes be reused across components? How to do serial and parallel control between multiple asynchronous processes?

Therefore, when there are many asynchronous processes, and asynchronous processes are not independent, there are serial, parallel, or even more complex relationships, directly put asynchronous logic in the component.

If not in components, where?

Can the middleware mechanism provided by Redux be used to accommodate these asynchronous processes?

Redux middleware

Let’s take a look at what Redux middleware is:

The redux process is simple: Dispatch an action to a store and reducer processes the action. So what if you want to do a little bit more processing before you get to the store? Where do I add it?

Transform dispatch! The idea behind middleware is to wrap dispatches in layers.

The following is the source code for applyMiddleware. ApplyMiddleware is a store.dispatch wrapper that returns a store with a modified Dispatch.

function applyMiddleware(middlewares) {
  let dispatch = store.dispatch
  middlewares.forEach(middleware= >
    dispatch = middleware(store)(dispatch)
  )
  return { ...store, dispatch}
}
Copy the code

So middleware looks like this:

function middlewareXxx(store) {
    return function (next) {
      return function (action) {
        // xx
      };
    };
  };
}
Copy the code

Middleware wraps dispatches, and dispatches pass the Action to the Store, so middleware naturally gets the Action, gets the Store, and the wrapped Dispatch, which is next.

For example, the redux-Thunk middleware implementation:

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

    return next(action);
  };
}

const thunk = createThunkMiddleware(); 
Copy the code

It determines that if action is a function, it executes that function and passes in store.dispath and store.getState, otherwise it passes to the inner dispatch.

With the Redux-Thunk middleware, we can put asynchronous procedures in a parameter to dispatch as a function:

const login = (userName) = > (dispatch) = > {
  dispatch({ type: 'loginStart' })
  request.post('/api/login', { data: userName }, () = > {
    dispatch({ type: 'loginSuccess'.payload: userName })
  })
}
store.dispatch(login('guang'))
Copy the code

But does this solve the problem that asynchronous processes in components are not easy to reuse, and that multiple asynchronous processes are not easy to do parallel, serial and other control problems?

No, this logic is still written in components, but moved to dispatches, and there is no mechanism to manage multiple asynchronous processes.

To solve this problem, use redux-Saga or Redux-Observable middleware.

redux-saga

Redux-saga does not change the action, it passes the action through the store, but adds an asynchronous process.

The Redux-Saga middleware is enabled like this:

import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'
import rootReducer from './reducer'
import rootSaga from './sagas'

const sagaMiddleware = createSagaMiddleware()
const store = createStore(rootReducer, {}, applyMiddleware(sagaMiddleware))
sagaMiddleware.run(rootSaga)
Copy the code

To run saga’s Watcher Saga:

Watcher Saga listens on some actions and calls worker Saga:

import { all, takeLatest } from 'redux-saga/effects'

function* rootSaga() {
    yield all([
      takeLatest('login', login),
      takeLatest('logout', logout)
    ])
}
export default rootSaga
Copy the code

Redux-saga would first pass an action to the store to determine whether it was listened to by Taker:

function sagaMiddleware({ getState, dispatch }) {
    return function (next) {
      return function (action) {
        const result = next(action);// Pass the action to the store

        channel.put(action); // Trigger the saga action listening process

        returnresult; }}}Copy the code

When the action is found to be monitored, execute the corresponding taker and call worker Saga to handle it:

function* login(action) {
  try {
      const loginInfo = yield call(loginService, action.account)
      yield put({ type: 'loginSuccess', loginInfo })
  } catch (error) {
      yield put({ type: 'loginError', error })
  }
}

function* logout() {
  yield put({ type: 'logoutSuccess'})}Copy the code

For example, login and logout will have different worker saga.

Login requests the LOGIN interface and triggers the loginSuccess or loginError action.

Logout triggers an action for logoutSuccess.

The asynchronous process management of Redux Saga is as follows: First pass the action to the store, and then determine whether the action is monitored by Taker. If it is, the corresponding worker Saga will be called to handle it.

Redux Saga adds a process to the Redux action process that listens for the asynchronous processing of the action.

Actually, the whole process is relatively easy to understand. The cost of writing a generator is high.

Take this code for example:

function* xxxSaga() {
    while(true) {
        yield take('xxx_action');
        / /...}}Copy the code

TakeEvery xxx_action = takeEvery xxx_action = takeEvery

function* xxxSaga() {
    yield takeEvery('xxx_action');
    / /...
}
Copy the code

While (true); while(true); while(true)

Isn’t. The generator returns an iterator and requires another program to call the next method to continue execution. So how it is executed and whether it continues is controlled by another program.

In Redux-Saga, the program that controls the execution of worker saga is called task. The worker saga simply tells the task what to do with the call, fork, put commands (these commands are called effects).

Task then calls different implementation functions to execute the worker saga.

Why is it designed this way? Just execute it directly. Why split it into worker Saga and Task?

It is true that designing as a generator increases the cost of understanding, but the trade-off is testability. Because various side effects, such as network requests, dispatch actions to stores, etc., become effects such as Call and PUT, which are controlled by the Task part. Then the specific execution can be switched at will. In this way, worker saga can be tested only by simulating the incoming corresponding data during the test.

The design of Redux Saga as a generator is a trade-off between learning costs and testability.

Remember what was wrong with Redux-Thunk? The parallel and serial complexity of multiple asynchronous processes cannot be handled. So how does Redux-Saga work out?

Redux-saga provides effects such as all, race, takeEvery, takeLatest to specify relationships between multiple asynchronous processes:

TakeEvery does the same for each action, takeLatest does the same for the last action, Race returns only the result of the fastest asynchronous process, and so on.

These effects, which control the relationship between multiple asynchronous processes, are what Redux-Thunk does not have, and are an essential part of the management of complex asynchronous processes.

So Redux-Saga can manage complex asynchronous processes and is very testable.

In fact, the most famous asynchronous process management is RXJS, and Redux-Observable is implemented based on RXJS, which is also a complex asynchronous process management scheme.

redux-observable

Redux-observable works much like Redux-Saga, such as enabling the plugin:

const epicMiddleware = createEpicMiddleware();

const store = createStore(
    rootReducer,
    applyMiddleware(epicMiddleware)
);

epicMiddleware.run(rootEpic);
Copy the code

It’s the same startup process as Redux Saga, only it’s called Epic instead of Saga.

However, for the processing of asynchronous processes, Redux Saga provides some effects by itself, while Redux-Observable is an operator using RXJS:

import { ajax } from 'rxjs/ajax';

const fetchUserEpic = (action$, state$) => action$.pipe(
  ofType('FETCH_USER'),
  mergeMap(({ payload }) => ajax.getJSON(`/api/users/${payload}`).pipe(
    map(response => ({
      type: 'FETCH_USER_FULFILLED',
      payload: response
    }))
  )
);
Copy the code

OfType specifies the action to listen on, and returns the action to store.

Compared with Redux-Saga, redux-Observable supports richer processing of asynchronous processes, which directly connects with operator ecology and is open, while Redux-Saga only provides several built-in effects for processing.

So redux-Observable can take advantage of RXJS operators when doing particularly complex asynchronous processes.

However, redux-Saga also has the advantages of good testability based on generator, and in most scenarios, the processing power of asynchronous process provided by Redux-Saga is sufficient, so redux-Saga is used more.

conclusion

The front-end framework implements the data to view binding, so we only need to worry about the data flow.

Redux’s view -> Action -> Store -> View one-way data flow is clearer and easier to manage than context’s chaotic data flow.

There are many asynchronous processes in the front-end code, which may have serial, parallel, or even more complex relationships that are not easily managed in components, but in Redux’s middleware.

Redux’s middleware is a layered wrapper around dispatches, such as Redux-thunk, which determines that an action is a function and then executes it, otherwise it continues to dispatch.

Redux-thunk does not provide a mechanism for managing multiple asynchronous processes. Complex asynchronous processes are managed using Redux-Saga or Redux-Observable.

Redux-saga transparently passes the action to the store and listens for the action to perform the corresponding asynchronous process. The description of asynchronous procedures is in the form of a generator for the benefit of testability. For example, use take, takeEvery, takeLatest to listen to the action and then execute the Worker saga. Worker saga can use effects such as put, call, and fork to describe different side effects, which are performed by task.

Redux-observable also listens to the action to perform the corresponding asynchronous process, but it is an RXJs-based operator. Compared with Saga, asynchronous process management is more powerful.

Whether Redux-Saga organizes asynchronous processes with generators or handles relationships between multiple asynchronous processes with built-in effects, Redux-observable also organizes relationships between asynchronous procedures and multiple asynchronous procedures using RXJS operators. Both of them solve the problem of processing complex asynchronous processes and can be selected flexibly according to the complexity of the scenario.