Асинхронные действия Redux с Redux Thunk

Введение

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

Существует две очень популярные библиотеки промежуточного ПО, поддерживающие побочные эффекты и асинхронные действия: Redux Thunk и Redux Saga. В этой статье мы расскажем о Redux Thunk.

Thunk или преобразователь — это концепция программирования, в которой функция используется для отсрочки оценки или расчета операции.

Redux Thunk — это промежуточное ПО, позволяющее вызывать создателей действий, которые возвращают функцию вместо объекта действия. Эта функция получает метод обработки магазина, который затем используется для обработки регулярных синхронных действий внутри тела функции после выполнения асинхронных операций.

Из этой статьи вы узнаете, как добавить Redux Thunk и использовать его для гипотетического приложения Todo.

Предварительные требования

Данная статья предполагает, что у вас имеются базовые знания по React и Redux. Вы можете посмотреть эту статью, если только начинаете работать с Redux.

Этот учебный модуль построен на основе гипотетического приложения Todo, которое отслеживает задачи, требующие выполнения, и уже выполненные задачи. Можно предположить, что приложение create-react-app было использовано для генерирования нового приложения React, и что redux, react-redux и axios уже установлены.

Здесь не разъясняются более подробные детали процедуры создания приложения Todo с нуля. Оно представлено как концептуальная основа для разъяснения Redux Thunk.

Добавление redux-thunk

Прежде всего, используйте терминал для перехода в каталог проекта и установите пакет redux-thunk в ваш проект:

Примечание. Redux Thunk содержит всего 14 строк кода. Посмотрите исходный код здесь, чтобы узнать о принципах работы промежуточного ПО Redux.

Примените промежуточное ПО при создании магазина вашего приложения, используя команду Redux applyMiddleware. Если использовать приложение React с redux и react-redux, ваш файл index.js будет выглядеть следующим образом:

src/index.js

import React from 'react'; import ReactDOM from 'react-dom'; import { Provider } from 'react-redux'; import { createStore, applyMiddleware } from 'redux'; import thunk from 'redux-thunk'; import './index.css'; import rootReducer from './reducers'; import App from './App'; import * as serviceWorker from './serviceWorker';  // use applyMiddleware to add the thunk middleware to the store const store = createStore(rootReducer, applyMiddleware(thunk));  ReactDOM.render(   <Provider store={store}>     <App />   </Provider>,   document.getElementById('root') ); 

Итак, мы импортировали Redux Thunk и добавили его в наше приложение.

Использование Redux Thunk в образце приложения

Чаще всего Redux Thunk используется для асинхронного взаимодействия с внешним API с целью получения или сохранения данных. Redux Thunk упрощает обработку действий, сопровождающих жизненный цикл запроса внешнего API.

Для создания нового элемента todo обычно требуется предварительно обработать действие, чтобы обозначить начало создания элемента todo. Затем, если элемент todo успешно создан и возвращен внешним сервером, необходимо обработать другое действие с новым элементом todo. В случае получения ошибки и невозможности сохранения todo на сервере необходимо обработать действие с ошибкой.

Давайте посмотрим, как сделать это с помощью Redux Thunk.

Импортируйте действие в компонент контейнера и обработайте его:

src/containers/AddTodo.js

import { connect } from 'react-redux'; import { addTodo } from '../actions'; import NewTodo from '../components/NewTodo';  const mapDispatchToProps = dispatch => {   return {     onAddTodo: todo => {       dispatch(addTodo(todo));     }   }; };  export default connect(   null,   mapDispatchToProps )(NewTodo); 

Действие использует Axios для отправки запроса POST на конечную точку в JSONPlaceholder (https://jsonplaceholder.typicode.com/todos):

src/actions/index.js

import {   ADD_TODO_SUCCESS,   ADD_TODO_FAILURE,   ADD_TODO_STARTED,   DELETE_TODO } from './types';  import axios from 'axios';  export const addTodo = ({ title, userId }) => {   return dispatch => {     dispatch(addTodoStarted());      axios       .post(`https://jsonplaceholder.typicode.com/todos`, {         title,         userId,         completed: false       })       .then(res => {         dispatch(addTodoSuccess(res.data));       })       .catch(err => {         dispatch(addTodoFailure(err.message));       });   }; };  const addTodoSuccess = todo => ({   type: ADD_TODO_SUCCESS,   payload: {     ...todo   } });  const addTodoStarted = () => ({   type: ADD_TODO_STARTED });  const addTodoFailure = error => ({   type: ADD_TODO_FAILURE,   payload: {     error   } }); 

Обратите внимание на то, как создатель действия addTodo возвращает функцию вместо обычного объекта действия. Эта функция получает метод обработки из магазина.

В теле функции вы вначале отправляете немедленное синхронное действие в магазин, чтобы показать, что вы начали сохранение элемента todo с внешним API. Затем вы отправляете на сервер фактический запрос POST, используя Axios. При успешном ответе сервера вы отправляете синхронное действие успеха с данными, полученными в составе ответа, однако в случае неудачи мы отправляем еще одно синхронное действие с сообщением об ошибке.

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

src/actions/index.js

// ...  export const addTodo = ({ title, userId }) => {   return dispatch => {     dispatch(addTodoStarted());      axios       .post(ENDPOINT, {         title,         userId,         completed: false       })       .then(res => {         setTimeout(() => {           dispatch(addTodoSuccess(res.data));         }, 2500);       })       .catch(err => {         dispatch(addTodoFailure(err.message));       });   }; };  // ... 

Чтобы протестировать сценарии ошибок, вы можете создать ошибку вручную:

src/actions/index.js

// ...  export const addTodo = ({ title, userId }) => {   return dispatch => {     dispatch(addTodoStarted());      axios       .post(ENDPOINT, {         title,         userId,         completed: false       })       .then(res => {         throw new Error('addToDo error!');         // dispatch(addTodoSuccess(res.data));       })       .catch(err => {         dispatch(addTodoFailure(err.message));       });   }; };  // ... 

Для полноты приведем пример того, как может выглядеть редуктор todo для обработки полного жизненного цикла запроса:

src/reducers/todosReducer.js

import {   ADD_TODO_SUCCESS,   ADD_TODO_FAILURE,   ADD_TODO_STARTED,   DELETE_TODO } from '../actions/types';  const initialState = {   loading: false,   todos: [],   error: null };  export default function todosReducer(state = initialState, action) {   switch (action.type) {     case ADD_TODO_STARTED:       return {         ...state,         loading: true       };     case ADD_TODO_SUCCESS:       return {         ...state,         loading: false,         error: null,         todos: [...state.todos, action.payload]       };     case ADD_TODO_FAILURE:       return {         ...state,         loading: false,         error: action.payload.error       };     default:       return state;   } } 

Изучение getState

После получения метода dispatch от состояния функция, возвращаемая создателем асинхронного действия с Redux Thunk, также получает метод getState магазина, что позволяет прочитать текущие значения магазина:

src/actions/index.js

export const addTodo = ({ title, userId }) => {   return (dispatch, getState) => {     dispatch(addTodoStarted());      console.log('current state:', getState());      // ...   }; }; 

В вышеперечисленном случае текущее состояние просто распечатывается на консоли.

Например:

{loading: true, todos: Array(1), error: null} 

Использование getState может быть полезно для того, чтобы обрабатывать действия по-разному в зависимости от текущего состояния. Например, если вы захотите ограничить возможности приложения одновременной обработкой только четырех элементов todo, вы можете выполнять возврат из функции, если состояние уже содержит максимальное количество элементов todo:

src/actions/index.js

export const addTodo = ({ title, userId }) => {   return (dispatch, getState) => {     const { todos } = getState();      if (todos.length > 4) return;      dispatch(addTodoStarted());      // ...   }; }; 

В вышеуказанном случае приложение сможет работать только с четырьмя элементами todo.

Заключение

В этом учебном модуле мы изучили добавление Redux Thunk в приложение React для асинхронной обработки действий. Это полезно при использовании магазина Redux и внешних API.

Если вы хотите узнать больше о React, почитайте нашу серию «Программирование на React.js» или посмотрите страницу тем React, где вы найдете упражнения и программные проекты.