Когда начинается новый SPA проект работа react-разработчика на начальной стадии сводится к выбору минимальных функциональных частей будущего приложения. Надо подобрать UI компоненты, библиотеку для работы с сетью, шаблон для взаимодействия компонентов разного уровня между собой. В качестве последнего сегодня в большинстве крупных проектов используется, конечно, Redux.

Redux - как контейнер состояния очень хорошо зарекомендовал себя в больших SPA, построенных как на react, так и на других современных SPA фреймворках. Сам по себе Redux, как шаблон, для понимании не сложный, но в реальные приложения на react не обходятся без side-эффектов и тут уже приходится добавлять компоненты для решения таких задач . Для простых side-эффектов это может быть redux-thunk, ну а для более комплексных - redux-saga. Настройка всех этих библиотек для работы как единое целое уже может представлять из себя не тривиальную задачу.  И тут на помощь приходит фреймворк dva.

DVA представляет собой небольшой фреймворк, который построен поверх redux и redux-saga. Он обладает всеми возможностями библиотеки redux-saga, но позволяет сделать код более структурированным и повысить его читаемость.

Базовые концепции

Ключевой особенностью данного фреймворка является модель состояния (state). Она представляет собой совокупность функций редьюсеров, функций side-эффектов и подписок (subscriptions). 

Рассмотрим схему работы фреймворка и код определения модели состояния:

const count = {
  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,
      };
    }
  },
  effects: {
    *addAsync(action, { call, put }) {
      yield call(delay, 1000);
      yield put({ type: 'add' });
    },
  },
  subscriptions: {
    keyboardWatcher({ dispatch }) {
      key('⌘+up, ctrl+up', () => { dispatch({type:'add'}) });
    },
  },
});

По сути модель представляет собой обычный javascript объект и является кусочком нашего глобального состояния (store) в терминологии Redux шаблона.

В данном объекте необходимо определить namespace, который будет определять название этого самого кусочка нашего глобального store.

Свойство state представляет собой простой объект и является состоянием, которое будет в дальнейшем маппится на какой-либо компонент. Свойство reducer представляет собой объект с функциями действиями, которые будут вызываться с помощью функции dispatch () при взаимодействии пользователя с компонентом. В данном свойстве описывается синхронный функционал, который не производит side-эффектов, так как редьюсеры должны быть чистыми функциями.

Свойство effects определяет функции генераторы, в которых и будут все наши асинхронные side-эффекты и уже из них вызываться функции reducer с помощью передачи соответствующего действия. Таким образом, если все это переводить на язык стандартного react-redux-saga шаблона и представить, что у нас есть начальное состояние store = {}, то:

1. namespace - store.count.

2. reducers - будут, по сути, соответствующими ветками в редьюсере.

3. effects - будут side-эффектами, которые мы обычно пишем в redux-saga.

Еще в модели присутствуют подписки (subscriptions), которые могут быть использованы для наблюдения за внешними событиями, типа смены роута или нажатия каких-либо клавиш на клавиатуре.

После определения модели (маленькой части нашего store) необходимо его подключить к компоненту. И тут приходит на помощь стандартная функция connect из react-redux:

const CountApp = ({count, dispatch}) => {
  return (
    <div className={styles.normal}>
      <div className={styles.record}>Highest Record: {count.record}</div>
      <div className={styles.current}>{count.current}</div>
      <div className={styles.button}>
        <button onClick={() => { dispatch({type: 'count/add'}); }}>+</button>
      </div>
      <div className={styles.button}>
        <button onClick={() => { dispatch({type: 'count/addAsync'}); }}>+ async</button>
      </div>
    </div>
  );
};

function mapStateToProps(state) {
  return { count: state.count };
}
const HomePage = connect(mapStateToProps)(CountApp);

После маппинга нашей маленькой части store к компоненту, у нас в нем также появляется функция dispatch, с помощью которой мы может вызывать смену состояния путем передачи ему необходимого действия. Заметьте, что тип действия начинается с namespace, а дальше идет название действия из reducers или effects. Т.е. 'count/addAsync'  попадет в функцию генератор addAsync, которая определена в свойстве effects

В теле данной функции доступны все функции redux-saga, остается только получить их с помощью деструкторизации и использовать. Можно получить часть store функцией select, можно вызвать асинхронный функционал функцией call, ну и, наконец, вызвать синхронный метод редьюсера функцией put.

Схема приложения

Привлекательной особенностью фреймворка является входная точка приложения (index.js). 

import dva from 'dva'
import 'babel-polyfill'
import Loading from 'dva-loading'

// 1. Initialize
const app = dva()

// 2. Add plugin
app.use(Loading(opt))

// 3. Model
app.model(require('./models/count'))

// 4. Router
app.router(require('./router'))

// 5. Start
app.start('#root')

В первом пункте происходит инициализация нашего приложения, которое может кастомизироваться различными плагинами. Например, добавлением автоматического отображения процесса загрузки на компоненты через модели (п.2). После присоединения данного плагина в моделях появятся свойства, которые будут отвечать за процесс загрузки данных, и мы можем использовать их в компонентах через маппинг состояния. Например, вот так: (state.loading.models.count).

function mapStateToProps(state) {
  return {
     loading: state.loading.models.count
  };
}
const HomePage = connect(mapStateToProps)(CountApp);

В третьем пункте мы присоединяем модели состояний к нашему приложению, определяя, таким образом, структуру нашего store. Остается только присоединить роутинг к приложению и отрендерить его в корневой элемент.

Роутинг можно присоединить стандартным способом, как делается в react приложениях:

<Router>
     <div>
          <Route exact path='/' component='{HomeComponent}'>
          <Route exact path='/about' component='{AboutComponent}'>
          <Route exact path='/count' component='{CountComponent}'>
     </div>
</Router>

А можно использовать функцию, предоставляемую фреймворком, которая сразу позволяет сделать динамический роутинг, т.е подгружать отдельные части javascript кода при активации пользователем определенного модуля приложения (меню, раздела...).

...
import dinamic form 'dva/dynamic';

function routerConfig({history, app}) {
    
     const HomeComponent = dynamic({
          app,
          models: () => {
               return [
                    import('./models/home')
               ]
          },
          component: () => import('./components/home')
     });

     const CountComponent = dynamic({
          app,
          models: () => {
               return [
                    import('./models/count')
               ]
          },
          component: () => import('./components/count')
     });

     return (
               <Router>
     <div>
          <Route exact path='/' component='{HomeComponent}'>
          <Route exact path='/count' component='{CountComponent}'>
     </div>
</Router>
     )
}

По большому счету, фреймворк делает за вас большую часть работы по настройке store, добавляя редьюсеры и мидлвары. Вам остается только сосредоточиться на создании своих моделей состояний, компонентов и сервисов для работы с сервером, т.е. по сути - на написании бизнес логики приложения.

Итоги

На мой взгляд, фреймворк дает весомые преимущества при разработки react приложений. К основным можно отнести:

  • Достаточно простой в концептуальном понимании, особенно если вы работали с redux.

  • Четкая и понятная архитектура приложения.

  • Простая, но в тоже время прозрачная организация входного скрипта приложения.

  • Хранение частей глобального store и логики их обработки в одном месте (модель).

  • Простое подключение динамической загрузки компонентов из коробки.

  • Имеются большие возможности кастомизации на различных стадиях работы приложения.