1 overview

The current popular data flow solutions are:

  • Flux, a one-way data flow scheme represented by Redux;
  • Reactive, a responsive data flow solution represented by Mobx;
  • Others, such as RXJS, etc.

2 the illustration Dva

Take a TodoList as an example, consisting of two parts: TodoList + Add Todo Button.

2.1 Primitive Farming — React

When you use the React state management capability without introducing a state management tool, the following practices are commonly used when data communication between different components is involved. The data to be shared is extracted to the nearest common ancestor, and the common ancestor uniformly maintains a state and the corresponding method to modify the state. The state and method are distributed to each sub-component in the form of props as needed. The child component listens for events and calls methods passed down from props to change the data in state.

2.2 Getting better — Redux

When the business is more complex, the above methods seem to be insufficient, this time needs to use professional state management tools. Here’s what Redux does:

  1. The state and page logic (reducer) are extracted from the reducer and become independent stores.
  2. Add a wrapper layer to and via the connect method to connect to store.
  3. The data in the store can be injected into the component as props.
  4. The component initiates an action with dispatch to the Store, which calls the corresponding Reducer to change state.
  5. When state is updated, connected components are refreshed as well.

In this way, the coupling degree between the state and view is reduced, facilitating expansion, unified data management, clear data flow, and convenient location when problems occur.

2.3 Adding wings to a Tiger — Saga

In actual project development, asynchronous network requests are indispensable, and the data updates above are synchronous. Use Middleware to block an action when it is dispatched, such as Redux-Saga:

  1. Click the Create Todo button to initiate an action of type == addTodo;
  2. Saga intercepts this action and initiates an HTTP request. If the request is successful, continue to send an action type == addTodoSucc to the reducer and write the data returned successfully to state through the corresponding reducer. Otherwise, send the action of type == addTodoFail and write the failed data to state through the corresponding reducer.

2.4 Starting out — Dva

It looks like everything is in place, and Dva is “best practice precipitation based on React + Redux + Saga”. Dva does two things to improve the coding experience:

  1. Unify store and Saga into one model concept and write it in one JS file;
  2. Added a Subscriptions, which collects actions from other sources, such as keyboard operations, etc.

Here is an example of a typical model:

app.model({
  namespace: "count".state: {
    record: 0.current: 0,},reducers: {
    add(state) {
      const newCurrent = state.current + 1;
      return {
        ...state,
        record: newCurrent > state.record ? newCurrent : state.record,
        current: newCurrent,
      };
    },
    minus(state) {
      return { ...state, current: state.current - 1}; }},effects: {*add(action, { call, put }) {
      yield call(delay, 1000);
      yield put({ type: "minus"}); }},subscriptions: {
    keyboardWatcher({ dispatch }) {
      key("⌘ + up and CTRL + up".() = > {
        dispatch({ type: "add"}); }); ,}}});Copy the code

Explanation of some key fields:

  • Namespace: indicates the namespace of the model.
  • State: Holds state data for the entire model, which can be any value, usually an object. Treating state as immutable data for each operation ensures that it is a new object and has no references. This ensures that state is independent and easy to test and track changes.
  • Reducers: Defines some functions on how to change the state, taking the old state and returning the new state each time. Reducer must be a pure function, so the same input must get the same output, and they should not have any side effects. Moreover, every calculation should use immutable data, which is simply understood to return a completely new (independent, pure) data per operation, so functions such as thermal overloading and time travel can be used.
  • Effects: Here we define the logic associated with side effects. In our application, the most common is asynchronous operations. It comes from the concept of functional programming, and it’s called a side effect because it makes our function impure, the same input doesn’t necessarily get the same output. In order to control the operation of side effects, DVA introduces Redux-Saga as the asynchronous process control at the bottom layer. As the related concepts of generator are adopted, the asynchronous writing method is changed into synchronous writing method, thus transforming Effects into pure function.
  • Subscriptions: Used to subscribe to a data source and dispatch required actions based on conditions. The data sources can be the current time, the server’s Websocket connection, keyboard input, geolocation changes, history route changes, and so on.

Other important concepts include:

  • Action: An object that describes how to change state, using Dispatch to initiate a change. Action must have the type attribute to specify the specific behavior. Other fields can be customized.
  • Dispatch: The props of any connected component will contain a dispatch function that initiates a change to state, taking action as an argument.
dispatch({
  type: 'user/add'.// If you call outside of model, you need to add a namespace
  payload: {}, // The information to be passed
});
Copy the code

Dva data flow diagram:

3 Source code Analysis

Version:

  • Dva: server – beta. 22
  • Story: 4.0.4
  • The react – redux: 7.2.3

To simplify the development experience, DVA has additional built-in React-Router and FETCH (which are not the focus of this analysis), so DVA can also be understood as a lightweight application framework. A minimal DVA project was created through DVA-CLI for this analysis (umi is now officially preferred).

As shown below, the page includes a button to get remote data (to try effects), and a list to display the data. When you click the Get Remote Data button, effect is executed and the returned data is displayed in a list. So here are some initial values.

3.1 create app

In the index.js file generated by the project, we first call the DVA method to create an app object, passing in the initial state. The app will serve as a main thread throughout (if you read the vue source code, it will sound familiar).

const app = dva({
  initialState: {
    products: [{name: "dva".id: 1 },
      { name: "antd".id: 2},],}});Copy the code

It’s worth mentioning that DVA splits itself into two parts, DVA and DVA-Core. The former implements the View layer with the high-order component React-Redux, while the latter solves the Model layer with Redux-Saga. Take a look at what the generated app contains:

{
  _models:[namespace: '@@dva', state, reducers: {'namespace/key': f}]
  _store: null._plugin: {_handleActions:null.hooks: {onError: [].onStateChange: [],... }},use: plugin.use
  model: Model function of corerouter: the router functionstart: start function}Copy the code

The _model here will be used later to hold all the models we define, and will initially include a model used internally by DVA to update the global state when unmodel is executed. 3.2 Adding plug-ins The second step adds some plug-ins. The previous step created a plugin instance and added the use method of the instance to the app to add some plug-ins.

app.use({});
Copy the code

3.3 load model

In this step, we add all models defined in the Model directory:

app.model(require("./models/products").default);

// ./models/products.js
export default {
  namespace: "products".state: [].reducers: {
    delete(state, { payload }) {
      return state.filter((item) = >item.id ! == payload); },setList(state, { payload }) {
      return[...state, ...payload]; }},effects: {*fetchList({ payload }, { put }) {
      const res = yield fetch(
        "https://cloudapi.bytedance.net/faas/services/ttt9zd/invoke/dva"
      );
      const jsonRes = yield res.json();

      yield put({
        type: "setList".payload: (jsonRes && jsonRes.data) || [], }); ,}}};Copy the code

This step is as simple as adding model to the app._model array:

app._models = [
  {namespace: ' ', state, reducers: {'@@dva/key': f, ... } {},namespace: ' ', state, reducers: {'products/key': f, ... },effects: {'products/key': f}}
]
Copy the code

It should be noted that DVA does some simple things to our model, Use the prefixNamespace method to prefix all the keys in reducers and Effects with ${namespace}/. .

3.4 Processing Routing Components

app.router(require("./router").default);

// router.js
import React from "react";
import { Router } from "dva/router";
import IndexPage from "./routes/IndexPage";

export default function RouterConfig({ history }) {
  return (
    <Router history={history}>
      <Switch>
        <Route path="/" exact component={IndexPage} />
      </Switch>
    </Router>
  );
}
Copy the code

This step is also relatively simple, simply attach the Router component to the app:

app._router = router
Copy the code

It is worth noting that the Connect component is involved in this process. For example, here is the indexPage component that needs to be rendered in the Router component:

import React from "react";
import { connect } from "dva";
import { Button, Popconfirm, Table } from "antd";

const Products = (props) = > {
  console.log("props", props);
  const { dispatch, products } = props;
  const columns = [
    {
      title: "Name".dataIndex: "name"}, {title: "Actions".render: (text, record) = > {
        return (
          <Popconfirm title="Delete?" onConfirm={()= > handleDelete(record.id)}>
            <Button>Delete</Button>
          </Popconfirm>); }},];function handleDelete(id) {
    dispatch({
      type: "products/delete".payload: id,
    });
  }

  function getRemote() {
    dispatch({
      type: "products/fetchList"}); }return (
    <div style={{ padding: "24px}} ">
      <Button
        type="primary"
        style={{ marginBottom: "24px"}}onClick={getRemote}
      >Get remote data</Button>
      <Table dataSource={products} columns={columns} />
    </div>
  );
};

export default connect(({ products }) = > ({ products }))(Products);
Copy the code

Connect (({products}) => ({products}))(products) Inject part of the state (products in this case) from the Dispatch and store into the props of the component. connect(…) (Products) internally recreates a component called ConnectFunction based on Products, whose WrappedComponent property points to Products. Wait until the ConnectFunction component executes to do some state injection (mapped to props), dispatch injection, and some props consolidation, subscriptions etc.

return useMemo(() = > {
  if (shouldHandleStateChanges) {
    // If this component is subscribed to store updates, we need to pass its own
    // subscription instance down to our descendants. That means rendering the same
    // Context instance, and putting a different value into the context.
    return (
      <ContextToUse.Provider value={overriddenContextValue}>
        {renderedWrappedComponent}
      </ContextToUse.Provider>)}return renderedWrappedComponent
}, [ContextToUse, renderedWrappedComponent, overriddenContextValue])
Copy the code

3.5 start

app.start("#root");
Copy the code

This step is critical, and DVA does several things:

  1. Call the start method of dVA-core
  2. Two middleware were created, a Saga middleware to handle asynchronous network requests and a Promise middleware to handle Effects
const sagaMiddleware = createSagaMiddleware();
const promiseMiddleware = createPromiseMiddleware(app);
Copy the code
  1. Collect the reducers and effects of all models in all app._models and do some processing:
app._getSaga = getSaga.bind(null);

const sagas = [];
constreducers = { ... initialReducer };// {router: connectRouter(history)}
for (const m of app._models) {
  reducers[m.namespace] = getReducer(m.reducers, m.state, plugin._handleActions);
  if (m.effects) {
    sagas.push(app._getSaga(m.effects, m, onError, plugin.get('onEffect'), hooksAndOpts)); }}Copy the code

Processing reducers is actually to reduce all reducers under the same namespace into a reducer. According to the convention on Model in DVA, after business modules are divided, the state of the same module is usually defined in a Model file, and each model is differentiated by namespace. There may be multiple reducer files under one namespace. The whole process of getReducer is as follows (this code is very succinct, but very informative) :

export default function getReducer(reducers, state, handleActions) {
  // Support reducer enhancer
  // e.g. reducers: [realReducers, enhancer]
  if (Array.isArray(reducers)) {
    return reducers[1]((handleActions || defaultHandleActions)(reducers[0], state));
  } else {
    return(handleActions || defaultHandleActions)(reducers || {}, state); }}function handleActions(handlers, defaultState) {
  const reducers = Object.keys(handlers).map(type= > handleAction(type, handlers[type]));
  constreducer = reduceReducers(... reducers);return (state = defaultState, action) = > reducer(state, action);
}

function handleAction(actionType, reducer = identify) {
  return (state, action) = > {
    const { type } = action;
    if (actionType === type) {
      return reducer(state, action);
    }
    return state;
  };
}

function reduceReducers(. reducers) {
  return (previous, current) = > reducers.reduce((p, r) = > r(p, current), previous);
}
Copy the code

Finally reducers[M.Namespace] will result in a function, which is the product of all the combinations of reducers under the namespace. Expressed in pseudocode as follows:

 (state = defaultState, action) => {
  let handleActions = [
    (state,action) = > {if (action.type === 'namespace/key1') return reducer(state,action)},
    (state,action) = > {if (action.type === 'namespace/key2') return reducer(state,action)},
  ]
  handleActions[0]()
  handleActions[1]()
}
Copy the code

If this namespace has an action, each reducer in the super-Reducer will execute it in turn. Each reducer will first determine whether the type in the action is consistent with its own type. If so, call reducer to return the new state; The new state becomes the next pawn parameter, and the process continues.

  1. Call the createStore method to create a store. In the previous step, a super Reducer was created for each namespace. First, combine all the super Reducer methods provided by Redux combineReducers. CreateStore combines all middleware and enhancers into an enhancer using Redux’s Compose. The enhancer can extend the store at the time it is created to make the store more third-party. Examples include Middleware, Time Travel, Persistence, etc. Finally, call redux’s createStore method to create the store.
app._store = createStore({
  reducers: createReducer(), // Redux combination function
  initialState: hooksAndOpts.initialState || {},
  plugin,
  createOpts,
  sagaMiddleware,
  promiseMiddleware,
});

// createStore
export default function({ reducers, initialState, plugin, sagaMiddleware, promiseMiddleware, createOpts: { setupMiddlewares = returnSelf }, }) {
  // extra enhancers
  const extraEnhancers = plugin.get('extraEnhancers');
  const extraMiddlewares = plugin.get('onAction');
  const middlewares = setupMiddlewares([
    promiseMiddleware,
    sagaMiddleware,
    ...flatten(extraMiddlewares),
  ]); // Add a routerMiddleware

  const enhancers = [applyMiddleware(...middlewares), ...extraEnhancers];
  returncreateStore(reducers, initialState, compose(... enhancers)); }Copy the code

The resulting Store will contain the following methods and then extend them a bit

app._store = {
  dispatch: ƒ (action); GetState: ƒ getState (); ReplaceReducer: ƒ replaceReducer (nextReducer); The subscribe: ƒ subscribe (the listener);Symbol(observables) : ƒ observables (); }const store = app._store;
store.runSaga = sagaMiddleware.run;
store.asyncReducers = {};
Copy the code
  1. Perform saga
sagas.forEach(sagaMiddleware.run);
Copy the code
  1. Dva’s internal history library provides three ways to create a history: CreateBrowserHistory, createMemoryHistory, createHashHistory, which is also a core dependency within the React-Router. Dva does some component rendering by subscribing to URL changes with history.listen.
setupApp(app);

app._history = patchHistory(history);
Copy the code
  1. Finally, add model and unmodel to the app
app.model = injectModel.bind(app, createReducer, onError, unlisteners);
app.unmodel = unmodel.bind(app, createReducer, reducers, unlisteners);
app.replaceModel = replaceModel.bind(app, createReducer, reducers, unlisteners, onError);
Copy the code
  1. The last step executes the Render method
// The container obtained earlier
if (isString(container)) {
  container = document.querySelector(container);
}

if (container) {
  render(container, store, app, app._router);
  app._plugin.apply('onHmr')(render.bind(null, container, store, app));
}

function render(container, store, app, router) {
  const ReactDOM = require('react-dom'); // eslint-disable-line
  ReactDOM.render(React.createElement(getProvider(store, app, router)), container);
}

function getProvider(store, app, router) {
  const DvaRoot = extraProps= > (
    <Provider store={store}>{router({ app, history: app._history, ... extraProps })}</Provider>
  );
  return DvaRoot;
}
Copy the code

4 summarizes

On the whole, DVA still relies on Redux and Saga for data flow management, with only a thin layer of encapsulation, but it lowers the threshold of state management in terms of development experience, much like VUEX. The organization of the whole code is also very clear, much like Vue, which also uses the singleton pattern, and then continuously adds global methods and variables to the APP object to expand the capability step by step, which is worth learning from.

5 Reference Materials

  1. Dvajs.com/guide/intro…
  2. www.yuque.com/flying.ni/t…