Understanding dva

Jun 27, 2021 · 4 min read · 797 Words · -Views -Comments

Some company projects use DvaJS (D.va). To use it better, I researched and summarized notes here.

Overview

React and redux based, lightweight and elm-style framework.

First and foremost, dva is a data flow solution based on redux and redux-saga. To simplify the development experience, dva also includes built-in react-router and fetch, so it can be understood as a lightweight application framework.

dva does three main things:

  • Unifies store and saga into a single “model” concept, written in one JavaScript file
  • Adds Subscriptions to collect actions from other sources, such as keyboard operations
  • Model syntax is very concise, similar to DSL or RoR, making coding very fast ✈️

So dva focuses on Redux‑based data flow, with built‑in react‑router and fetch.

Usage

dva.js

// products-model.js
export default {
  // Key on global state
  namespace: 'products',

  // Initial value, here is an empty array
  state: [],

  // Equivalent to reducer in redux, receives action, updates state synchronously
  reducers: {
    'delete'(state, { payload: id }) {
      return state.filter(item => item.id !== id);
    },
  },
  
  subscriptions: {
    
  }
};
// Initialize dva application
const app = dva({
  initialState: {
    products: [
       { name: 'dva', id: 1 },
      { name: 'antd', id: 2 },
    ],
   },
 });
// Register view
           
app.model(productsModel.default);

app.router(() => <ConnectedApp />);

// Start the application
app.start('#root');

Model reuse

dva-model-extend

Utility method to extend dva model.

https://github.com/dvajs/dva-model-extend

import modelExtend from 'dva-model-extend';

const human = {
  state: {
    stomach: null,
  },
  reducers: {
    eat(state, { payload: food }) {
      return { ...state, stomach: food };
    },
  },
};

const benjy = modelExtend(human, {
  namespace: 'human.benjy',
  state: {
    name: 'Benjy',
  },
});

Source

The project uses lerna for multi-package management

├── dva // Official React bindings for dva, with react-router@4.
├── dva-core // The core lightweight library for dva, based on redux and redux-saga.
├── dva-immer // Create the next immutable state tree by simply modifying the current tree
└── dva-loading // Auto loading data binding plugin for dva. :clap: You don't need to write `showLoading` and `hideLoading` any more.

Code size

JavaScript source code in /packages, statistics script cloc . --md --exclude-dir=test --include-ext=js

Languagefilesblankcommentcode physical lines
JavaScript26128110962
——–——–——–——–——–
SUM:26128110962

The small code size suggests a lightweight abstraction.

Key functions

dva()

// packages/dva/src/index.js
export default function(opts = {}) {
  const history = opts.history || createHashHistory();
  const createOpts = {
    initialReducer: {
      router: connectRouter(history),
    },
    setupMiddlewares(middlewares) {
      return [routerMiddleware(history), ...middlewares];
    },
    setupApp(app) {
      app._history = patchHistory(history);
    },
  };

  const app = create(opts, createOpts);
  //
  const oldAppStart = app.start;
  app.router = router;
  app.start = start;
  return app;
	...
}

model()

// packages/dva-core/src/index.js 
function injectModel(createReducer, onError, unlisteners, m) {
    m = model(m);

    const store = app._store;
    store.asyncReducers[m.namespace] = getReducer(m.reducers, m.state, plugin._handleActions);
    store.replaceReducer(createReducer());
    if (m.effects) {
      store.runSaga(app._getSaga(m.effects, m, onError, plugin.get('onEffect'), hooksAndOpts));
    }
    if (m.subscriptions) {
      unlisteners[m.namespace] = runSubscription(m.subscriptions, m, app, onError);
    }
  }

subscriptions

// packages/dva-core/src/subscription.js
export function run(subs, model, app, onError) {
  const funcs = [];
  const nonFuncs = [];
  // Loop through and execute each subscription function
  for (const key in subs) {
    if (Object.prototype.hasOwnProperty.call(subs, key)) {
      const sub = subs[key];
      const unlistener = sub(
        {
          dispatch: prefixedDispatch(app._store.dispatch, model),
          history: app._history,
        },
        onError,
      );
      if (isFunction(unlistener)) {
        funcs.push(unlistener);
      } else {
        nonFuncs.push(key);
      }
    }
  }
  return { funcs, nonFuncs };
}

start()

// packages/dva/src/index.js 
function start(container) {
    // Allow container to be a string, then use querySelector to find the element
    if (isString(container)) {
      container = document.querySelector(container);
      invariant(container, `[app.start] container ${container} not found`);
    }

// Validate container type
    invariant(
      !container || isHTMLElement(container),
      `[app.start] container should be HTMLElement`,
    );

// Check if router is registered
    invariant(app._router, `[app.start] router must be registered before app.start()`);


    //
    if (!app._store) {
      oldAppStart.call(app);
    }

    const store = app._store;

    // export _getProvider for HMR
    // ref: https://github.com/dvajs/dva/issues/469
    app._getProvider = getProvider.bind(null, store, app);

    // If has container, render; else, return react component
    if (container) {
      render(container, store, app, app._router);
// Handle hot module replacement
      app._plugin.apply('onHmr')(render.bind(null, container, store, app));
    } else {
      return getProvider(store, this, this._router);
    }
  }

plugin/use()

In dva, classes are used in only two places: dynamic component loading and plugin definitions

  use(plugin) {
    invariant(isPlainObject(plugin), 'plugin.use: plugin should be plain object');
    const { hooks } = this;
    for (const key in plugin) {
      if (Object.prototype.hasOwnProperty.call(plugin, key)) {
        invariant(hooks[key], `plugin.use: unknown plugin property: ${key}`);
        if (key === '_handleActions') {
          this._handleActions = plugin[key];
        } else if (key === 'extraEnhancers') {
          hooks[key] = plugin[key];
        } else {
          hooks[key].push(plugin[key]);
        }
      }
    }
  }

Final Thoughts

Reading the source code shows that dva’s encapsulation is relatively shallow, but it solves the logic organization problem around redux, while providing routing configuration/subscriptions and plugin extensions.

References

Authors
Developer, digital product enthusiast, tinkerer, sharer, open source lover