Redux Saga

Что такое Redux Saga ?

Redux saga - это библиотека, которая призвана упростить управление побочными эффектами приложения (то есть асинхронными вещами, такими как выборка данных и нечистыми вещами, такими как доступ к кешу браузера), более эффективным в исполнении, легким для тестирования и лучшим в обработке ошибок.

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

Интересно

Redux-saga - это redux middleware, что означает, что этот поток может быть запущен, приостановлен и отменен из основного приложения с помощью обычных действий redux, он имеет доступ к полному состоянию приложения redux, а также может отправлять действия redux.

Redux Saga «Генераторы», чтобы легко обрабатывать асинхронные потоки для чтения, записи и тестирования. Таким образом, асинхронные потоки в вашем приложении выглядят как ваш стандартный синхронный код JavaScript. (вроде async / await)

Установка

npm install --save redux-saga
const News =(props) => {
const onNewsClick = ({id}) {
dispatch({type: 'NEWS/FETCH_BY_ID/REQUEST', payload: {id}})
}
...
}

Компонент отправляет обычное действие объекта в store. Мы создадим сагу, которая отслеживает все действия NEWS/FETCH_BY_ID/REQUEST и запускает вызов API для получения данных.

sagas.js

import { call, put, takeEvery, takeLatest } from "redux-saga/effects";
import Api from "...";
//worker Saga: будет запущен при действиях NEWS/FETCH_BY_ID/REQUEST
function* fetchNewsById(action) {
try {
const news = yield call(Api.fetchNews, action.payload.id);
yield put({ type: "NEWS/FETCH_BY_ID/RESOLVED", payload: news });
} catch (e) {
yield put({ type: "NEWS/FETCH_BY_ID/REJECTED", message: e.message });
}
}
/*
Запускает fetchNewsById при каждом отправленном действии NEWS/FETCH_BY_ID/REQUEST.
Разрешает одновременные выборки пользователя.
*/
function* mySaga() {
yield takeEvery("NEWS/FETCH_BY_ID/REQUEST", fetchNewsById);
}
/*
В качестве альтернативы вы можете использовать takeLatest.
Не разрешает одновременную загрузку пользователя. Если "NEWS/FETCH_BY_ID/REQUEST" получит
отправляется, когда выборка уже ожидает, эта ожидающая выборка отменяется
и будет запущена только последняя версия.
*/
function* mySaga() {
yield takeLatest("NEWS/FETCH_BY_ID/REQUEST", fetchNewsById);
}
export default mySaga;

Чтобы запустить нашу Saga, нам нужно будет подключить ее к Redux Store с помощью промежуточного программного обеспечения redux-saga.

import { createStore, applyMiddleware } from "redux";
import createSagaMiddleware from "redux-saga";
import reducer from "./reducers";
import mySaga from "./sagas";
// создаем saga middleware
const sagaMiddleware = createSagaMiddleware();
// Подключаем сагу к store
const store = createStore(reducer, applyMiddleware(sagaMiddleware));
// запускаем сагу
sagaMiddleware.run(mySaga);

Redux saga эффекты

Запуск побочных эффектов изнутри саги всегда осуществляется путем получения некоторого декларативного эффекта.

Сага фактически объединяет все эти эффекты вместе для реализации желаемого потока управления. Самый простой пример - это упорядочить полученные эффекты, помещая значения урожайности один за другим. Вы также можете использовать знакомые операторы потока управления (if, while, for) для реализации более сложных потоков управления.

SAGA API

TAKE

take(pattern);

Создает описание эффекта, которое указывает saga middleware ждать указанного действия в store.

Генератор приостанавливается до тех пор, пока не будет отправлено действие, соответствующее шаблону (pattern).

Результатом yield take (pattern) является отправляемый объект action.

pattern интерпретируется по следующим правилам:

  • Если take вызывается без аргументов или '*' совпадают все отправленные action (например, take () будет соответствовать всем action)

  • Если это функция, действие сопоставляется с шаблоном и если возвращаеться true (например, take (action => action.entities) будет соответствовать всем действиям, имеющим (true) поле entities).

  • Если это строка, action соответствует, если action.type === pattern (например, take (INCREMENT_ASYNC)

  • Если это массив, каждый элемент в массиве сопоставляется с вышеупомянутыми правилами, поэтому поддерживается смешанный массив строк и предикатов функций. Однако наиболее распространенный вариант использования - это массив строк, так что action.type сопоставляется со всеми элементами в массиве (например, take ([INCREMENT, DECREMENT]), и это соответствует действиям типа INCREMENT или DECREMENT).

put

put(action);

Создает описание эффекта, которое указывает middleware запланировать отправку действия в store. Эта отправка может быть не немедленной, поскольку другие задачи могут находиться впереди в очереди задач саги или все еще выполняться.

call

call(fn, ...args);

Создает описание эффекта, которое указывает middleware вызывать функцию fn с args в качестве аргументов.

fn: Function - функция-генератор или обычная функция, которая либо возвращает Promise в качестве результата, либо любое другое значение. args: Array <any> - массив значений, передаваемых в качестве аргументов функции fn

fork

fork(fn, ...args);

Создает описание эффекта, которое указывает middleware выполнить неблокирующий вызов функции fn.

takeEvery

takeEvery(pattern, saga);

Создает сагу для каждого action, отправленном из store, которое соответствует шаблону.

pattern: Строка | Массив | Функция

saga: Function - функция генератора

В примере ниже мы создадим базовую задачу fetchProducts. Мы используем takeEvery для запуска новой задачи fetchProducts при каждом отправленном действии FETCH_PRODUCTS:

import { takeEvery } from `redux-saga/effects`
function* fetchProducs(action) {
...
}
function* watchFetchProducts() {
yield takeEvery('FETCH_PRODUCTS', fetchProducs)
}

takeEvery позволяет обрабатывать одновременные действия. В приведенном выше примере, когда отправляется действие FETCH_PRODUCTS, новая задача fetchProducs запускается, даже если предыдущий fetchProducs все еще ожидает обработки

note

takeEvery не обрабатывает неупорядоченные ответы от задач. Нет гарантии, что задачи завершатся в том же порядке, в котором они были запущены. Чтобы обрабатывать неупорядоченные ответы, вы можете рассмотреть takeLatest.

Реализация takeEvery - выглядит следующим образом:

const takeEvery = (pattern, saga) =>
fork(function* () {
while (true) {
const action = yield take(pattern);
yield fork(saga, action);
}
});

takeLatest

takeLatest(pattern, saga);

Формирует сагу для каждого action, отправленном из store, которое соответствует шаблону. И автоматически отменяет любую предыдущую задачу саги, запущенную ранее, если она все еще выполняется.

Каждый раз, когда в store отправляется action. И если это действие соответствует шаблону, takeLatest запускает новую задачу саги в фоновом режиме. Если задача саги была запущена ранее (последним действием, отправленным перед фактическим действием), и если эта задача все еще выполняется, задача будет отменена.

pattern: Строка | Массив | Функция

saga: Function - функция генератора

Пример Cоздадим задачу fetchProducs. Мы используем takeLatest для запуска новой задачи fetchProducs при каждом отправленном действии FETCH_PRODUCTS. Поскольку takeLatest отменяет любую отложенную задачу, запущенную ранее, это гарантирует, что если пользователь быстро запускает несколько последовательных действий FETCH_PRODUCTS, будет завершено только последнее действие.

import { takeLatest } from 'redux-saga/effects'
function* fetchProducs(action) {
...
}
function* watchFetchProducts() {
yield takeLatest('FETCH_PRODUCTS', fetchProducs)
}

Реализация takeLatest - выглядит следующим образом:

const takeLatest = (pattern, saga) =>
fork(function* () {
let lastTask;
while (true) {
const action = yield take(pattern);
if (lastTask) {
yield cancel(lastTask); // отмена не выполняется, если задача уже завершена
}
lastTask = yield fork(saga, action);
}
});

cancel

cancel(task);

Создает описание эффекта, которое указывает middleware отменить ранее разветвленную задачу.

task: Task - объект задачи, возвращенный предыдущим fork;

cancel - это неблокирующий эффект. т.е. выполнение саги возобновится сразу после вызова cancel.

Пример 👀

Почитать 📚

Практика 👩‍💻👨‍💻

Используя код переписать его с использование redux saga

Edit this page on GitHub