Skip to content

Latest commit

 

History

History
1058 lines (868 loc) · 43.9 KB

Flux-Redux-Vuex-Mobx.md

File metadata and controls

1058 lines (868 loc) · 43.9 KB

Flux

Fluxархитетура построения пользовательских интерфейсов, основанная на шаблоне "Наблюдатель" (observer pattern, EventEmitter).
Изначально разработана компанией Facebook для React и React Native приложений.

Особенности Flux

  • Однонаправленный поток данных:
    Action Creator -> Action -> Dispatcher -> Callbacks -> Stores -> Views.
    Action создаётся при взаимодействии пользователя со View, но может создаваться и самим приложением.
  • Может быть несколько Stores.
  • Store может быть как изменяемым (mutable), так и неизменяемым (immutable).
  • В приложении может быть только один Dispatcher, региструющий все Callbacks.

Структура Flux

Actionобъект, описывающий происходящее в приложении действие.

Action обязательно должен иметь тип (type), может иметь дополнительную информацию (если полей несколько, то для удобства можно их объединить в одно поле-объект, например payload: { /* ... */ }).

const CHANGE_FETCH_STATUS = 'CHANGE_FETCH_STATUS';
const FETCH_ITEMS = 'FETCH_ITEMS';

Пример Action.

const FETCH_ITEMS_ACTION = ({
  type: FETCH_ITEMS,
  items: [1, 3],
});

Action Creatorфункция-строитель, которая создаёт Action в зависимости от переданных ей аргументов.

const changeFetchStatus = isFetching => ({
  type: CHANGE_FETCH_STATUS,
  isFetching,
});

const fetchItems = () => FETCH_ITEMS_ACTION;

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

const store = {
  items: [],
  isFetching: false,
};

Flux допускает наличие нескольких Stores.

Callback (в контексте Flux) — функция, которая принимает Action и в зависимости от него обновляет или не обновляет часть данных, лежащих в Stores.

const itemCallback = (action) => {
  if (action.type === CHANGE_FETCH_STATUS) {
    store.isFetching = action.isFetching;
  } else if (action.type === FETCH_ITEMS) {
    store.items = action.items;
  }
} 

Каждый Callback должен быть зарегистрирован при помощи Dispatcher.

Dispatcher — это модуль или класс, который позволяет регистировать (register) Callbacks и вызывать их всех с параметром Action каждый раз, когда вызывается функция dispatch.

const dispatcher = new Dispatcher();

dispatcher.register(itemCallback);

// store: { items: [], isFetching: false }
dispatcher.dispatch(changeFetchStatus(true)); 
// store: { items: [], isFetching: true }
dispatcher.dispatch(fetchItems()); 
// store: { items: [1, 3], isFetching: true }
dispatcher.dispatch(changeFetchStatus(false)); 
// store: { items: [1, 3], isFetching: false }

// если Action не обрабатывается ни в одном Callback, то Store должен остаться без изменений
dispatcher.dispatch({ type: 'INCORRECT_ACTION' }); 
// store: { items: [1, 3], isFetching: false }

Во Flux разрешено, чтобы Action Creator вызывал dispatch сразу при создании Action (так можно упростить код выше):

const fetchItems = () => dispatcher.dispatch({
  type: FETCH_ITEMS,
  items: [1, 3],
});

View — UI-компонента (обычно React-компонента).

const Button = (<button onClick={fetchItems}>Fetch items</button>);

Redux

Redux — библиотека, основанная на подходе Flux и вносящая в него некоторые правки.

Три основных принципа Redux

  • Только один источник правды (single source of truth) — Store.
  • Состояние доступно только для чтения (state is read-only). Его можно изменить лишь с помощью Actions.
  • Изменение состояния происходит только с использованием чистых функций (pure functions) — Reducers.

Другие особенности Redux

  • Однонаправленный поток данных:
    Action Creator -> Action -> Reducers -> Store -> Views.
  • Store только один, но он может разбиваться на части, за каждую из которых отвечает отдельный Reducer.
  • Store должен быть неизменяемым (immutable). Reducer не изменяет state напрямую, а работает с копией и возвращает её.
  • Отсутствует Dispatcher, вместо него используется функция store.dispatch().
  • В схеме Redux изначально нет места асинхронным функциям, но есть возможность это исправить, подключив middleware (thunk, saga).

Структура Redux

Части Action, Action Creator, Store и View определяются аналогично подходу Flux за исключением того, что в Redux может быть только один Store, а Action Creator не может вызывать функцию dispatch, при необходимости за него это делает Bound Action Creator:

const ADD_ITEM = 'ADD_ITEM';

/* Action Creator */
const addItem = item => ({ type: ADD_ITEM, item });
dispatch(addItem(5)); // dispatch action ADD_ITEM

/* Bound Action Creator */
const boundAddItem = item => dispatch(addItem(item));
boundAddItem(5) // dispatch action ADD_ITEM

Reducerчистая функция, определяющая, как изменится состояние приложения (state) в ответ на Action.
Reducer имеет вид: (previousState, action) => newState, что напоминает функцию reduce(), откуда и название.

const reduce = (accumulator, currentValue) => accumulator + currentValue;
const initialState = {
  items: [],
};

const itemReducer = (state = initialState, action) => {
  switch (action) {
    case ADD_ITEM:
      return ({
        ...state,
        items: [
          ...state.items,
          action.item,
        ],
      });

    default:
      return state;
  }
};

Почему используется деструктуризация ... и нельзя просто напрямую изменить значение в state?
Потому что объекты и массивы в JavaScript передаются по ссылке.
Если изменить их значение напрямую, то произойдёт мутация и Redux не заметит изменений, а значит оно не дойдёт до View в нужный момент.

Когда Action не обрабатывается в текущем Reducer, то он попадает в блок default.
Состояние должно остаться без изменений, поэтому оно возвращается без деструктуризации.

В Redux может быть несколько Reducers, каждый из которых отвечает за какую-то часть State.
Reducers комбинируются в главный Reducer, который передаётся в Store при создании:

import { createStore, combineReducers } from 'redux';

/* ... */

const reducer = combineReducers({
  item: itemReducer,
  another: anotherReducer,
});

const store = createStore(reducer);
/*
const store = {
  item: { ... },
  another: { ... },
};
*/

Как и в случае Callbacks из Flux, все Reducers получают приходящий Action.
Тем не менее между Reducer и Callback есть существенное отличие:
Callback не принимает в параметрах и не возвращает state (как это делает Reducer), вместо этого он изменяет state напрямую.

const itemState = {
  items: [],
};

const itemCallback = (action) => {
  if (action.type === ADD_ITEM) {
    itemState.items = [
      ...items,
      action.item,
    ];
  }
};

В Redux отсутствует Dispatcher, его работу берёт на Store, предоставляя функцию dispatch.

// item's state: { items: [] }
store.dispatch(addItem(7));
// item's state: { items: [7] }
console.log(store.getState())

Vuex

Vuexшаблон управления состоянием (state management pattern) приложения, а также библиотека, разработанная для Vue.js приложений.

Особенности Vuex

  • Однонаправленный поток данных:
    Action -> Mutations -> State -> Views.
    Action создаётся при взаимодействии пользователя со View, но может создаваться и самим приложением.
  • Может быть только один State, но в нём могут быть выделены отдельные независимые блокиModules.
  • State изменяем (mutable), единственный способ его изменитьсовершить (commit) Mutation.
  • Action может использовать асинхронные функции, Mutation — не может.

Структура Vuex

Stateобъект, в котором хранится состояние приложения.
Располагается в Store и инициализируется вместе с ним.

import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);
const store = new Vuex.Store({
  state: {
    items: [],
  },
});

Чтобы State был доступен во всех компонентах, нужно встроить Store в приложение.

  const app = new Vue({
    /* ... */
    store,
  });

Использование State внутри компонент: this.$store.state.

const Items = {
  template: `<ul>
  <li v-for="item in items">
    {{ item }}
  </li>
</ul>`,
  computed: {
    items () {
      return this.$store.state.items,
    }
  }
}

Getterвспомогательная функция, принимающая State и возвращающая некоторую его часть или же вычисляющая на его основании что-то новое. Параметры Getter: (state, getters), stateссылка на store.state, getters — другие Getters, которые можно использовать.

const store = new Vuex.Store({
  /* ... */,
  getters: {
    itemsLength: state => state.items.length,
    numbers: state => state.items.filter(item => typeof item === 'number'),
    getItemByIndex: state => index => state.items[index],
  },
});

Использование Getters.

store.getters.itemsLength;
store.getters.getItemByIndex(0);

// в компоненте
this.$store.getters.numbers;

Mutationфункция, в которой происходит изменение State.
Единственный способ изменить Stateсовершить (commit) Mutation.
Mutationсинхронная транзакция, для асинхронности используется Action.
Параметры Mutation: (state, payload), stateссылка на store.state, payloadобъект с переданными параметрами.

// mutation-types.js
export const ADD_ITEM = 'ADD_ITEM';
// store.js
import { ADD_ITEM } from './mutation-types';

const store = new Vuex.Store({
  /* ... */,
  mutations: {
    [ADD_ITEM] (state, payload) {
      // мутируем State (не можем испозовать state.items.push(), потому
      // что должна измениться ссылка на массив, произойти мутация)
      state.items = [...state.items, item];
    },
  },
});

Совершение Mutation: commit(type, payload).

// state: { items: [] }
store.commit(ADD_ITEM, { item: 7 });
// или
store.commit({ type: ADD_ITEM, item: 7 });
// state: { items: [7] }

// в компоненте
this.$store.commit(ADD_ITEM, { item: 7 });

Actionфункция, которые совершает (commit) в своём теле Mutations и может выполнять асинхронные операции.
Параметры Action: (context, payload), context — объект, который содержит:

  • state — то же, что и store.state
  • commit(type, payload) — функция для совершения Mutation
  • dispatch(type, payload) — функция для отправки Action
  • gettersGetters
import api from '/* ... */'; // связующее звено между клиентом и сервером

const store = new Vuex.Store({
  /* ... */,
  actions: {
    /* синхронный Action */
    addItem (context, payload) {
      context.commit('addItem', payload.item);
    },
    /* асинхронный Action */
    async fetchItem (context) {
      const item = await api.get(/*...*/); // получение item откуда-либо, допустим получили 7
      
      context.dispatch('addItem', { item });
      // или
      context.dispatch({ type: 'addItem', item });
    },
  },
});

Отправка (dispatch) Action: dispatch(type, payload).

// state: { items: [] }
store.dispatch('addItem', 5);
// state: { items: [5] }
store.dispatch('fetchItem');
// state: { items: [5, 7] }

// в компоненте
this.$store.dispatch('fetchItem');

View — UI-компонента (Vue.js)

Modules позволяют создавать независимые блоки глобального State, в которых есть свои локальные State, Actions, Getters и Mutations.

const itemModule = {
  state: { /* ... */ },
  mutations: { /* ... */ },
  actions: { /* ... */ },
  getters: { /* ... */ },
};

const userModule = {
  state: { /* ... */ },
  mutations: { /* ... */ },
  actions: { /* ... */ },
  getters: { /* ... */ },
};

const store = new Vuex.Store({
  modules: {
    item: itemModule,
    user: userModule,
  }
});

store.state.item // item's state
store.state.user // user's state

Структура локальных Mutations, Actions, Getters такая же, только теперь в качестве state берётся локальный State.
В локальных Actions и Getters есть возможность обратиться к глобальным State и Getters при помощи rootState и rootGetters.

const itemModule = {
  state: {
    items: [],
  },
  getters: {
    itemsLength (state, getters, rootState, rootGetters) { /* ... */ },
  },
  mutations: {
    ADD_ITEM (state, payload) {
      state.items = [...state.items, item];
    },
  },
  actions: {
    addItem (context, payload) {
      context.rootGetters;
    },
  },
};

По умолчанию Actions, Getters, Mutations вызываются так, словно они были определены в глобальном State.
Это удобно, если все их названия уникальны, но может стать проблемой в большом приложении, где названия могут совпадать.

store.commit('ADD_ITEM');
store.dispatch('addItem');
store.getters.itemsLength;

Чтобы это исправить, можно замкнуть функционал в Module, задав ему namespace.

const itemModule = {
  namespaced: true,
  /* ... */
};

const store = Vuex.Store({
  modules: {
    item: itemModule,
  },
});

Тогда в обращение добавится префикс с названием модуля, а предыдущее обращение перестанет работать.

store.commit('item/ADD_ITEM');
store.dispatch('item/addItem');
store.getters['item/itemsLength'];

Modules можно подключать и удалять динамически (многие Vue.js плагины подключают свои Modules так к приложению).

const adminModule = { /* ... */ };

/* подключение модуля */
store.registerModule('user', userModule);
/* удаление модуля */
store.unregisterModule('user');

Module может содержать другие Modules.

const moderatorModule = { /* ... */ };

const userModule = {
  modules: {
    moderator: moderatorModule,
  },
};

В больших приложениях State может сильно разрастаться (становится неудобно и сложно его поддерживать), модули помогают решить эту проблему и сделать приложение более расширяемым.

MobX

MobX — библиотека, помогающая просто и гибко управлять состоянием приложения, основанная на подходе прозрачного реактивного функционального программирования (Transparent functional reactive programming, TFRP).

Сам по себе MobX не является архитектурой или хранилищем состояния, но обладает функционалом, с помощью которого можно это всё построить, из-за чего его часто считают альтернативой Redux.

Основной принцип MobX

Всё, что может быть извлечено (be derived) из состояния приложения, должно быть извлечено. Автоматически.

Речь идёт о текущем состоянии приложения: при его обновлении должно автоматически обновиться всё, что его использовало.

Суть того, к чему стремится реактивное программирование, можно показать на примере (достигается при помощи Observable).

a = 5
b = 7
c = a + b // с = 12

b = 10 // c = 15
a -=4 // c = 11

Особенности MobX

  • Однонаправленный поток данных:
    Actions -> State -> Derivations (computed values) -> Reactions.
  • State представлен любым Observable, изменения которого отслеживаются при помощи Reactions; из-за этого State децентрализован, раздроблен.
  • State изменяем (mutable).
  • Нужные Derivations пересчитываются и Reactions вызываются автоматически при изменении State.

Структура MobX

State

Stateданные приложения (объекты, массивы, примитивы), составляющие в совокупности Модель приложения (как в подходах по типу MVC).

Любое значение, которое в приложении имеет тип Observable и его изменения отслеживаются при помощи Reactions, является частью State.

Синтаксис:

  • observable(value) (только для массивов и объектов)
  • observable.box(value) (для примитивных значений)
  • @observable property = value (только в классах)
import { observable } from 'mobx';

// массивы и объекты
const items = observable([1, 2, 3]);
const item = observable({ title: 'Foo', description: 'Bar' });

// примитивы
const string = observable.box('string');
const number = observable.box(7);
const boolean = observable.box(true);

// Observable.box создаёт объект. Для получения примитивного значения нужно использовать метод get()
console.log(number.get()) // 7

// внутри классов можно использовать декораторы (официально пока ещё экспериментальные) или функции
class ItemStore {
  @observable item = { title: 'Foo', description: 'Bar' }
  @observable string = 'string' // в декораторе примитив без .box
  /* или */
  item = observable({ title: 'Foo', description: 'Bar' })
  string = observable.box('string')
}

Action

Action — всё, что изменяет State.

В отличие от архитектуры Flux и её производных, MobX не ставит ораничений на то, как должны быть обработаны события пользователя.

Простые Actions для массивов и объектов

const item = observable({ title: 'foo', description: 'bar' });
const items = observable([1, 2, 3]);

// Actions
item.title = 'new';
item.title = `${item.title} title`;
item.description = 'new description';
items.pop()
items.push(4)
items[1] = 8;

Простые Actions для примитивов:

let number = observable.box(1);

// Action
number.set(3); // в консоль выведется 'Changed to 3'

// как ниже делать нельзя, поскольку это перезапишет переменную number, сделав её обычным примитивом
number = 2; // в консоль не выведется ничего
number.set(4); // TypeError: number.set is not a function

MobX сам заботится, чтобы все изменения State, произошедшие при помощи Action, автоматически обработались в Derivations и Reactions.

Если строить архитектуру управления состоянием при помощи MobX, то лучше всего использовать встроенный функционал action.

  • action(fn)
  • action(name, fn)
  • @action classMethod()

В этом случае так же следует запретить любые изменения State (то есть изменения любого Observable) вне Actions.

import { configure } from 'mobx';

configure({ enforceActions: 'always' });

С такой конфигурацией простые примеры выше с Actions не будут работать (после появления Reactions), поскольку будет ошибка: Error: [mobx] Since strict-mode is enabled, changing observed observable values outside actions is not allowed.

Нужно изменить код следующим образом:

import { observable, action } from 'mobx';

const item = observable({ title: 'foo', description: 'bar' });
const items = observable([1, 2, 3]);

const changeItem = action(({ title, description } = {}) => {
  item.title = title;
  item.description = description;
  
  // нельзя переприсваивать, поскольку item перестанет быть Observable
  item = { title, description };
});

const addItem = action(item => items.push(item));
let number = observable.box(1);
// set - автоматически является action, но если его добавить в функцию, то это уже работать не будет
number.set(3); // может работать
const set = value => number.set(value); // не работает
const set = action(value => number.set(value)); // работает

Derivation

Derivation (computed values) — любое значение, которое может вычисляется автоматически после обновления State.
Derivation может быть переменной, UI-компонентой и многим другим.
Derivations используются в приложении, как и Observables.

На практике Derivation — результат выполнения функции, которая имеет тип Computed.
В этой функции не должно быть сайд-эффектов (side effects).

Синтаксис:

  • computed(() => derivation)
  • @computed get property() { return derivation; } (только в классе)
const statement = observable.box(3 > 1); // true

const oppositeStatement = computed(() => !statement.get());

console.log(oppositeStatement); // false
trueStatement.set(false);
console.log(oppositeStatement); // true

Reaction

Reaction — это функция, которая запускается автоматически после обновления State.
Reaction похож на Derivation, но вместо генерации нового значения в нём обрабатываются сайд-эффекты: вывод в консоль, запросы к серверу и прочее.

// Автоматически вызывается для всех Observable значений, использующихся в сайд-эффектах
autorun(() => { /* side effects here */ }));

// подписка на изменения
autorun(() => console.log(item)); // сработает только при инициализации (ссылка на объект не меняется) и выведет в консоль Observable
autorun(() => console.log('Title changed', item.title)); // при обновлении поля поля title объекта item
autorun(() => console.log('Title or description changed', item.title, item.description)); // при обновлении одного из полей title или description объекта item
autorun(() => console.log('Data changed', { ...item })); // при обновлении любого поля объекта item

// подписка на изменения
// Observable — объект. Чтобы получить значение переменной number, нужно использовать number.get()
autorun(() => console.log('Changed to', number.get()); 

autorun(() => { console.log('Item changed', item) });

when(() => condition, () => { /* side effects here */ });

reaction(() => data, data => { /* side effects here */ });

reaction(
  () => arr.map(item => [item.a, item.b]),
  data => console.log("CHANGED A,B:", data)
);

При использовании MobX с React наиболее популярный Reaction — observer.
Он оборачивает метод класса render() или функциональный компонент в autorun, тогда в случае изменения любого Observable внутри них, компонент автоматически перерендерится.

import { Fragment } from 'react';
import { render } from 'react-dom';
import { observable } from 'mobx';
import { observer } from 'mobx-react';

const state = observable({
  number: 0,
});

const increment = () => {
  state.number += 1;
};

// пример функциональной компоненты
const Number = observer(({ state }) => (
  <button onClick={increment}>Click me to increase: {state.number}</button>
));

// пример класса
@observer
class NumberClass extends React.Component {
  render() {
    return <button onClick={increment}>Click me to increase: {this.props.state.number}</button>;
  }
}

const App = () => (
  <Fragment>
    <Number state={state} />
    <NumberClass state={state} />
  </Fragment>
);

render(<App />, document.getElementById('root'));

Дополнительно

Реализация Flux от Facebook и её использование

Функционал Dispatcher:

  • register(callback: function): string — регистрирует Callback, возвращает его идентификатор id.
  • dispatch(action: object): void — отправляет Action во все зарегистрированные Callbacks.
  • isDispatching(): boolean — возвращает true, если происходит отправка (dispatching) в данный момент, false иначе.
  • waitFor(ids: string[]): void — ожидает выполения Callbacks, имеющих идентификаторы ids, прежде, чем продолжать выполнять текущий Callback.
  • unregister(id): void — разрегистрирует Callback по id.

Функционал ReduceStore:

  • getState(): T — получение полного состояния текущего Store. Если Store неизменяемый (immutable), то следует переопределить метод и не передавать состояние напрямую.
  • getInitialState(): T — задание начального состояния текущего Store. Вызывается только один раз: во время создания.
  • reduce(state: T, action: object) — изменяет или не изменяет текущее состояние в зависимости от Action. Метод обязательно должен быть переопределён; должен быть чистым (pure), без сайд-эффектов.
  • areEqual(one: T, two: T): boolean — проверяет, совпадают ли две версии состояния. Если Store неизменяемый, то не нужно переопределять этот метод.
import { Dispatcher } from 'flux';
import { ReduceStore } from 'flux/utils';

const ItemDispatcher = new Dispatcher();

/* Action Creators */
export const changeFetchStatus = isFetching => ItemDispatcher.dispatch({
  type: 'CHANGE_FETCH_STATUS',
  isFetching,
});

export const fetchItems = () => ItemDispatcher.dispatch({
  type: 'FETCH_ITEMS',
  items: [1, 3],
});

interface IItemStore {
  items: any[];
  isFetching: boolean;
}

class ItemStore extends ReduceStore<IItemStore> {
  constructor() {
    super(ItemDispatcher);
  }

  getInitialState(): IItemStore {
    return ({
      items: [],
      isFetching: false,
    });
  }

  getState(): IItemStore {
    return ({
      ...this._state,
    });
  }

  reduce(state: IItemStore, action: object): IItemStore {
    switch (action.type) {
      case 'CHANGE_FETCH_STATUS':
        return ({
          ...state,
          isFetching: action.isFetching,
        });
    
      case 'FETCH_ITEMS':
        return ({
          ...state,
          items: [...action.items],
        });
    
      default:
        return state;
    }
  }
}

export default new ItemStore();

Redux Thunk vs Redux Saga

Redux Thunk

Thunk — функция, которая оборачивает выражение, чтобы отложить его выполнение.

// обычный Action Creator
const doSomething = params => ({ type: 'DO_SOMETHING', ...params });

// обычный Bound Action Creator
const boundDoSomething = params => dispatch({ type: 'DO_SOMETHING', ...params });

В обоих случаях нет места асинхронным функциям.

Также можно заметить, что функция dispatch(action) сама по себе возвращает Action, то есть результат вызова функций doSomething() и boundDoSomething() одинаков: возвращается Action типа DO_SOMETHNG.

Библиотека redux-thunk вместо возвращения Action или вызова dispatch(action) возвращает функцию, что позволяет отложить вызов функции dispatch(action), а перед вызовом проводить какие-то дополнительные операции, в том числе и асинхронные.

// Action Creator при использовании redux-thunk
const doSomething = params => dispatch => dispatch({ type: 'DO_SOMETHING', ...params });

// Action Creator c асинхронной операцией при использовании redux-thunk
const doSomething = params => async (dispatch) => {
  await new Promise(resolve => setTimeout(resolve, 1000));
  return dispatch({ type: 'DO_SOMETHING', ...params });
};

Приятным бонусом является то, что в функции мы можем контролировать возвращаемое значения, поскольку не обязательно возвращать результат выполнения dispatch(action)).

const doSomething = params => async (dispatch) => {
  await new Promise(resolve => setTimeout(resolve, 1000));
  dispatch({ type: 'DO_SOMETHING', ...params });
  const data = { /* ... */ };
  return data;
};

Таким образом можно вернуть какие-то данные в компоненту (например, пойманные ошибки).

Redux Saga

Saga — архитектурный паттерн их микросервисной архитектуры для реализации транзакции, охватывающей (span) несколько сервисов.

Внутри сервиса доступны ACID-транзакции, но между несколькими сервисами их использовать нельзя, поэтому и используется Saga, последовательно запускающая локальные транзакции в каждом сервисе. В случае, если какая-нибудь локальная транзакция из последовательности не проходит, Saga отменяет всю последовательность при помощи компенсирующих транзакций.

Redux Saga реализована как промежуточный слой (middleware), позволяющий проделывать сайд-эффекты (например, запросы к серверу), при помощи ES6-генераторов.

Функция-генератор возрващает объект-итератор. Каждый вызов метода итератора next() выполняет код до следующего выражения yield и останавливается.

function* addTwo(x) {
  yield x;
  for (let i = 0; i < 2; i++) {
    yield x += 1;
  }
}

let it = addTwo(0);
console.log(it.next()); // { value: 0, done: false }
console.log(it.next()); // { value: 1, done: false }
console.log(it.next()); // { value: 2, done: false }
console.log(it.next()); // { value: undefined, done: true }

Для началы работы с сагами необходимо создать сагу-наблюдателя и сагу-рабочего.

Сага-наблюдатель (Watcher Saga) следит за Actions и создаёт новую задачу на каждый отправленный Action при помощи вспомогательной функции takeEvery.

import { takeEvery } from 'redux-saga/effects'

function* watchFetchArticles() {
  yield takeEvery('FETCH_ARTICLES', fetchArticles);
}

Эффект (Effect) — простой JavaScript-объект (plain JS Object), содержащий необходимые инструкции, которые должен выполнить redux-saga middleware. Например: отправить (dispatch) Action, вызвать асинхронную функцию.

Сага-рабочий (Worker Saga) использует эффекты, чтобы обработать Action.

import axios from 'axios';
import { call, put } from 'redux-saga/effects'

function* fetchArticles() {
  try {
    const articles = yield call(axios.get, '/articles');
    yield put({ type: 'FETCH_SUCCESS', payload: articles });
  } catch (error) {
    yield put({ type: 'FETCH_ERROR', payload: error });
  }
}

В примере выше используются два основных эффекта.
Первым эффектом является call(fn, ...args), описывающий асинхронный HTTP-запрос.

{
  CALL: {
    fn: axios.get,
    args: ['/articles']
  }
}

Вторым — put(action), заменяющий dispatch.

{
  PUT: {
    action: { type: 'FETCH_SUCCESS', payload: articles }
  }
}

Таким образом, вместо вызова сайд-эффектов напрямую

function* fetchArticles() {
  const articles = yield axios.get('/articles');
  dispatch({ type: 'FETCH_SUCCESS', articles })
}

используются Эффекты, являющиеся простыми объектами, что вносит большую гибкость и позволяет с лёгкостью их тестировать.

Если есть несколько отправленных одновременно Actions, то takeEvery вызывает несколько экземпляров саги-рабочего, наделяя саги свойством параллелизма (concurrency).

Использование React с Redux

Возможная структура

Для маленьких проектов и быстрых решений:

  - resources/
  -- item.js
  -- store.js

Хорошо расширяемая структура (разбиение по смыслу):

 - resources/
 -- item/
 --- item.actions.js
 --- item.reducer.js
 --- item.selectors.js
 --- item.types.js
 -- store.js

Также расширяемая, но очень удобная (разбиение по типу файлов):

 - resources/
 -- actions/
 --- item.actions.js
 -- reducers/
 --- item.reducer.js
 -- selectors/
 --- item.selectors.js
 -- types/
 --- item.types.js
 -- store.js

Настройка

  1. Устанавливаем зависимости
npm i redux react-redux redux-thunk
  1. Создаём основные компоненты:

Types

export const DELETE_ITEM = 'DELETE_ITEM';

Actions (Actions, Action Creators, Bound Action Creators)

/* item.actions */
import { DELETE_ITEM } from './item.types';

const deleteItem = id => dispatch => dispatch({ type: DELETE_ITEM, id });

Reducer

/* item.reducer.js */

const initialState = {
  items: [],
  /* ... */
};

const itemReducer = (state = initialState, action) => { /* ... */ };

export default itemReducer;

Selectors (вспомогательные функции для получения некоторой части state)

/* item.selectors */

/* state - все данные в Store
   item - название Reducer, отвечающего за данные для этой компоненты
   items - массив элементов */
const getItemById = (state, id) => state.item.items.find(item => item.id === id);
const getItemsLength = state => state.item.items.length;
  1. Создаём Store, комбинируя Reducers и добавляя redux-thunk в качестве middleware:
/* store.js */
import {
  createStore,
  combineReducers,
  applyMiddleware,
} from 'redux';
import thunk from 'redux-thunk';
import { itemReducer } from './item/item.reducer';

const reducer = combineReducers({ /* ... */ });

const store = createStore(reducer, applyMiddleware(thunk));

export default store;
  1. Оборачиваем главную компоненту <App> компонентой <Provider>, в которую передаём созданный Store.
import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import App from './App'
import store from './store'

const AppContainer = () => (
  <Provider store={store}>
    <App />
  </Provider>
);

render(<AppContainer />, document.getElementById('root'));
  1. Оборачиваем компоненту, которую хотим подключить к Redux, компонентой высшего порядка при помощи функции connect(mapStateToProps, mapDispatchToProps).
  • mapStateToProps(state, props) отвечает за передачу части Store в props оборачиваемой компоненте (эта часть будет передаваться каждый раз после обновления в Store).
  • mapDispatchToProps отвечает за оборачивание переданных ему Action Creators в функцию dispatch (чтобы в оборачиваемой компоненте не нужно было вызывать dispatch).
import { connect } from 'react-redux';
import { deleteItem } from 'resources/item/item.actions';
import { getItemsLength, getItemById } from 'resources/item/item.selectors';

const ItemView = ({
  item,
  itemsLength,
  dispatchDeleteItem
}) => (
  <>
    <div class="item">{item}</div>
    <div class="index">`Item 1 of ${itemsLength}`</div>
    <button onClick={dispatchDeleteItem}> 
  </>
);

// пусть props.id - параметр, который передаётся от родительской компоненты или от роутера
const mapStateToProps = (state, props) => ({
  item: getItemById(state, props.id),
  itemsLength: getItemsLength(state),
});

const mapDispatchToProps = {
  dispatchDeleteItem: deleteItem,
};

export default connect(mapStateToProps, mapDispatchToProps);