Permalink
Switch branches/tags
Nothing to show
Find file Copy path
917 lines (689 sloc) 20.5 KB

the dva.js Knowledgemap

Notice:if you are using dva.js@2, please ignore the section below about Router, updating on the way...

Some common confusions where first started learning React.js or dva.js:

  • should I understand all the ES6 new features?
  • There are three different ways to create a React component, should I learn all of them?
  • How to add/edit/remove Reducer state?
  • How to handle global/local error?
  • How to send asynchronous request?
  • How to handle rather complex async process logic?
  • How to setup Router?
  • ...

This article provides you with all the practical information to understand dva.js and it's command line tool dva-cli, so you can start building projects like dva-hackernews in no time without all the unnecessary extras.

Table of Contents

JavaScript

Variables declaration

const vs let

Use const and let instead of var. const means that the identifier can’t be reassigned. let is used for a reassignable variable. Beware the difference that var is scoped to a function, const and let are both scoped to the block。

const DELAY = 1000;

let count = 0;
count = count + 1;

Template literals

Template literals (string interpolation and multi-line strings) provides convenient ways for coding strings

const user = 'world';
console.log(`hello ${user}`);  // hello world

// multi-line string
const content = `
  Hello ${firstName},
  Thanks for ordering ${qty} tickets to ${event}.
`;

Default function parameters

function logActivity(activity = 'skiing') {
  console.log(activity);
}

logActivity();  // skiing

Arrow Function

Concise syntax for writing function expressions, without writing function and return keywords. Arrow functions are anonymous and change the way this binds in functions.

An arrow function does not have its own this; the this value of the enclosing execution context is used.

For example:

[1, 2, 3].map(x => x + 1);  // [2, 3, 4]

Equivalent to:

[1, 2, 3].map((function(x) {
  return x + 1;
}).bind(this));

Module import and export

Use import for module import and export for module export.

For example:

// import whole
import dva from 'dva';

// partial importation
import { connect } from 'dva';
import { Link, Route } from 'dva/router';

// import whole and assign to github
import * as github from './services/github';

// default export
export default App;
// partial exportation and using import { App } from './file'
export class App extend Component {};

ES6 Object and Array

Destructing

Using destructing to extract data from Object or Array, and assign to variable concisely.

// Object
const user = { name: 'guanguan', age: 2 };
const { name, age } = user;
console.log(`${name} : ${age}`);  // guanguan : 2

// Array
const arr = [1, 2];
const [foo, bar] = arr;
console.log(foo);  // 1

destructing can also be applied to variables in function parameter

const add = (state, { payload }) => {
  return state.concat(payload);
};

destructing allows alias assignment for meaningful naming

const add = (state, { payload: todo }) => {
  return state.concat(todo);
};

Object literals improvement

The opposite operation to destructing, used to construct a new Object

const name = 'duoduo';
const age = 8;

const user = { name, age };  // { name: 'duoduo', age: 8 }

The function keyword can be omitted when defining Object methods

app.model({
  reducers: {
    add() {}  // equivalent to add: function() {}
  },
  effects: {
    *addRemote() {}  // equivalent to addRemote: function*() {}
  },
});

Spread Operator

Spread Operator or ... operator, can be used in several scenarios:

Constructing new Array with extra data, equivalent to push

const todos = ['Learn dva'];
[...todos, 'Learn antd'];  // ['Learn dva', 'Learn antd']

Or extracting data from array literal, thinking of slice

const arr = ['a', 'b', 'c'];
const [first, ...rest] = arr;
rest;  // ['b', 'c']

// With ignore
const [first, , ...rest] = arr;
rest;  // ['c']

Constructing Array from function arguments

function directions(first, ...rest) {
  console.log(rest);
}
directions('a', 'b', 'c');  // ['b', 'c'];

Replacing apply

function foo(x, y, z) {}
const args = [1,2,3];

//Two equivalent expression:
foo.apply(null, args);
foo(...args);

Constructing (updating) new Object. (ES2017 stage-2 proposal)

const foo = {
  a: 1,
  b: 2,
};
const bar = {
  b: 3,
  c: 2,
};
const d = 4;

const ret = { ...foo, ...bar, d };  // { a:1, b:3, c:2, d:4 }

Also in JSX, Spread Operator can be used for adding new props, please refer to Spread Attributes.

Promises

Better async request handling with Promise, for example fetch async request:

fetch('/api/todos')
  .then(res => res.json())
  .then(data => ({ data }))
  .catch(err => ({ err }));

Define Promise

const delay = (timeout) => {
  return new Promise(resolve => {
    setTimeout(resolve, timeout);
  });
};

delay(1000).then(_ => {
  console.log('executed');
});

Generators

effects in dva.js are achieved using generator, a generator function returns a Generator object, and it conforms to both the iterable protocol and the iterator protocol, generator function executed until the yield expression.

Below is a typical dva.js effect, which provides asynchronous flow control by using yield:

app.model({
  namespace: 'todos',
  effects: {
    *addRemote({ payload: todo }, { put, call }) {
      yield call(addTodo, todo);
      yield put({ type: 'add', payload: todo });
    },
  },
});

React Component

Stateless Functional Components

There are three different ways to create React Component, React.createClass, class and Stateless Functional Component. Use Stateless Functional Component whenever you can, keep it clear and immutable. Notice that Stateless Functional Component is not a Object, it's functional programming, a pure function, without state, hence no use for this.

To define App Component for instance:

function App(props) {
  function handleClick() {
    props.dispatch({ type: 'app/create' });
  }
  return <div onClick={handleClick}>${props.name}</div>
}

Equivalent to:

class App extends React.Component {
  handleClick() {
    this.props.dispatch({ type: 'app/create' });
  }
  render() {
    return <div onClick={this.handleClick.bind(this)}>${this.props.name}</div>
  }
}

JSX

Component Composition

Similar to HTML tag, Components can be nested in JSX.

<App>
  <Header />
  <MainContent />
  <Footer />
</App>

className

Please beware using className instead of class for JSX styling, as class is preserved in JavaScript.

<h1 className="fancy">Hello dva</h1>

JavaScript Expressions

JavaScript expressions are wrapped inside pairs of curly brackets{} for JSX, it will execute and return.

For example:

<h1>{ this.props.title }</h1>

Mapping Arrays to JSX

Here is a way to map an array to JSX:

<ul>
  { this.props.todos.map((todo, i) => <li key={i}>{todo}</li>) }
</ul>

Comments in JSX

Avoid using // for single line comments.

<h1>
  {/* multiline comment */}
  {/*
    multi
    line
    comment
    */}
  {
    // single line
  }
  Hello
</h1>

Spread Attributes

A quite useful feature that JSX borrowed from ECMAScript6, using Spread Operator to extend Component's props.

For instance:

const attrs = {
  href: 'http://example.org',
  target: '_blank',
};
<a {...attrs}>Hello</a>

Equivalents to:

const attrs = {
  href: 'http://example.org',
  target: '_blank',
};
<a href={attrs.href} target={attrs.target}>Hello</a>

Props

Data handling is a key concept in React and it can be overwhelming for beginners. Data handles through props, state or context in React. But when using dva.js, all you need is just props, one of strengths that dva.js provides over React.

propTypes

Since JavaScript is weakly typed, please declare props' types using propTypes for type validation.

function App(props) {
  return <div>{props.name}</div>;
}
App.propTypes = {
  name: React.PropTypes.string.isRequired,
};

Built-in props type:

  • PropTypes.array
  • PropTypes.bool
  • PropTypes.func
  • PropTypes.number
  • PropTypes.object
  • PropTypes.string

Passing data from parent to child

Passing data from child to parent

CSS Modules

Understanding CSS Modules

Visual illustration of CSS Modules mechanism:

button class will be renamed to ProductList_button_1FU0u after execution, button is a local name, ProductList_button_1FU0u is the global name. so you can use simple descriptive name, without worrying about conflict

After that, all you need to do is create related styles of .button {...} in css/less file, import and refer it by calling styles.button.

Defining Global CSS

CSS Modules are default to local scopes, to declare a global style, using :global syntax

For example:

.title {
  color: red;
}
:global(.title) {
  color: green;
}

Calling by:

<App className={styles.title} /> // red
<App className="title" />        // green

classnames Package

When dealing with some complex situations, each element can have multiple className and each className may also conditional dependent, when this is the case, classnames library will be handy.

import classnames from 'classnames';
const App = (props) => {
  const cls = classnames({
    btn: true,
    btnLarge: props.type === 'submit',
    btnSmall: props.type === 'edit',
  });
  return <div className={ cls } />;
}

Thus will create different className when passing different types to App Component

<App type="submit" /> // btn btnLarge
<App type="edit" />   // btn btnSmall

Reducer

reducer is a function, which takes a state and a action, output a state: (state, action) => state

add/edit/delete

Using todos as example:

app.model({
  namespace: 'todos',
  state: [],
  reducers: {
    add(state, { payload: todo }) {
      return state.concat(todo);
    },
    remove(state, { payload: id }) {
      return state.filter(todo => todo.id !== id);
    },
    update(state, { payload: updatedTodo }) {
      return state.map(todo => {
        if (todo.id === updatedTodo.id) {
          return { ...todo, ...updatedTodo };
        } else {
          return todo;
        }
      });
    },
  },
};

add/edit/delete in nested data structure

For best practice and keep a flat state, maximum of one layer nesting is recommended, deep nesting are prone to bug and maintenance disaster.

app.model({
  namespace: 'app',
  state: {
    todos: [],
    loading: false,
  },
  reducers: {
    add(state, { payload: todo }) {
      const todos = state.todos.concat(todo);
      return { ...state, todos };
    },
  },
});

Below is an example of deep nesting, try to avoid this:

app.model({
  namespace: 'app',
  state: {
    a: {
      b: {
        todos: [],
        loading: false,
      },
    },
  },
  reducers: {
    add(state, { payload: todo }) {
      const todos = state.a.b.todos.concat(todo);
      const b = { ...state.a.b, todos };
      const a = { ...state.a, b };
      return { ...state, a };
    },
  },
});

Effect

For instance:

app.model({
  namespace: 'todos',
  effects: {
    *addRemote({ payload: todo }, { put, call }) {
      yield call(addTodo, todo);
      yield put({ type: 'add', payload: todo });
    },
  },
});

Effects

put

For action triggering.

yield put({ type: 'todos/add', payload: 'Learn Dva' });

call

For asyncs, with Promise supporting.

const result = yield call(fetch, '/todos');

select

For extract data from state.

const todos = yield select(state => state.todos);

Error Handling

Global Error Handler

Error throws for effects and subscriptions in dva.js are uing onError hook, hence errors can be handled in batch with onError

const app = dva({
  onError(e, dispatch) {
    console.log(e.message);
  },
});

The error threw out of effects and promise rejects will all be captured.

Local Error Handler

Using try catch inside effects, when special error handling is needed for specific effects.

app.model({
  effects: {
    *addRemote() {
      try {
        // Your Code Here
      } catch(e) {
        console.log(e.message);
      }
    },
  },
});

Asynchronous Request

Asynchronous Requests are based on whatwg-fetch, please refer to the API docs here: https://github.com/github/fetch

GET and POST

import request from '../util/request';

// GET
request('/api/todos');

// POST
request('/api/todos', {
  method: 'POST',
  body: JSON.stringify({ a: 1 }),
});

default error handling

Executing the default error handling when backend promise returns in following format:

{
  status: 'error',
  message: '',
}

Edit utils/request.js, add the following middleware:

function parseErrorMessage({ data }) {
  const { status, message } = data;
  if (status === 'error') {
    throw new Error(message);
  }
  return { data };
}

By this, errors will using onError hook.

Subscription

subscriptions will subscribe to data source, and dispatch according to different actions. Data source can be current time, server's websocket connection, keyboard event, changes in geolocation, changes in history router, etc, with format of ({ dispatch, history }) => unsubscribe.

Initialization with Asynchronous Data

For example: when a user enter /users page, users/fetch action will be triggered to load user data.

app.model({
  subscriptions: {
    setup({ dispatch, history }) {
      history.listen(({ pathname }) => {
        if (pathname === '/users') {
          dispatch({
            type: 'users/fetch',
          });
        }
      });
    },
  },
});

path-to-regexp Package

For a complex url path, like /users/:userId/search, it's rather hard to match or get userID. path-to-regexp is recommended.

import pathToRegexp from 'path-to-regexp';

// in subscription
const match = pathToRegexp('/users/:userId/search').exec(pathname);
if (match) {
  const userId = match[1];
  // dispatch action with userId
}

Router

Config with JSX Element (router.js)

<Route path="/" component={App}>
  <Route path="accounts" component={Accounts}/>
  <Route path="statements" component={Statements}/>
</Route>

For details: react-router

Route Components

Route Components are referred to files under ./src/routes/, which are matching with the Components under ./src/router.js.

data binding using connect

For example:

import { connect } from 'dva';
function App() {}

function mapStateToProps(state, ownProps) {
  return {
    users: state.users,
  };
}
export default connect(mapStateToProps)(App);

Thus App Component will receive dispatch and users props.

Injected Props (e.g. location)

Route Component will receive router information from extra props.

  • location
  • params
  • children

Please refer to this: react-router

Routing by action

import { routerRedux } from 'dva/router';

// Inside Effects
yield put(routerRedux.push('/logout'));

// Outside Effects
dispatch(routerRedux.push('/logout'));

// With query
routerRedux.push({
  pathname: '/logout',
  query: {
    page: 2,
  },
});

Methods are not limited to push(location), please refer to: react-router-redux

dva configuration

Redux Middleware

For example, adding redux-logger Middleware:

import createLogger from 'redux-logger';
const app = dva({
  onAction: createLogger(),
});

Notice: onAction support array literal, can pass multiple Middlewares.

history

use browserHistory instead of history

import { browserHistory } from 'dva/router';
const app = dva({
  history: browserHistory,
});

disable _k query in hashHistory

import { useRouterHistory } from 'dva/router';
import { createHashHistory } from 'history';
const app = dva({
  history: useRouterHistory(createHashHistory)({ queryKey: false }),
});

Tools

create dva.js app using dva-cli

First, install dva-cli globally.

$ npm install dva-cli -g

Second, create a new dva.js app: myapp.

$ dva new myapp

Then open myapp dict and start

$ cd myapp
$ npm start