Как делать асинхронные Redux экшены используя Redux-Thunk

Автор оригинала: Alligator.io
  • Перевод
  • Tutorial

Приветствую Хабр! Представляю вашему вниманию перевод статьи — Asynchronous Redux Actions Using Redux Thunk, автора — Alligator.io


По умолчанию, экшены в Redux являются синхронными, что, является проблемой для приложения, которому нужно взаимодействовать с серверным API, или выполнять другие асинхронные действия. К счастью Redux предоставляет нам такую штуку как middleware, которая стоит между диспатчом экшена и редюсером. Существует две самые популярные middleware библиотеки для асинхронных экшенов в Redux, это — Redux Thunk и Redux Saga. В этом посте мы будем рассматривать первую.

Redux Thunk это middleware библиотека, которая позволяет вам вызвать action creator, возвращая при этом функцию вместо объекта. Функция принимает метод dispatch как аргумент, чтобы после того, как асинхронная операция завершится, использовать его для диспатчинга обычного синхронного экшена, внутри тела функции.

Если вам интересно, то Thunk, это концепт в мире программирования, когда функция используется для задержки выполнения операции.

Установка и настройка


Во первых, добавьте redux-thunk пакет в ваш проект:

$ yarn add redux-thunk
# или, с помощью npm:
$ npm install redux-thunk

Затем, добавьте middleware, когда будете создавать store вашего приложения, с помощью applyMiddleware, предоставляемый Redux'ом:

index.js

import React from 'react';
import ReactDOM from 'react-dom';
import { createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import thunk from 'redux-thunk';

import rootReducer from './reducers';
import App from './App';

// используй applyMiddleware, чтобы добавить thunk middleware к стору
const store = createStore(rootReducer, applyMiddleware(thunk));

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

Основное использование


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

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

Давайте посмотрим, как это может быть реализовано с помощью Redux-Thunk. В компоненте, экшен диспатчится как обычно:

AddTodo.js

import { connect } from 'react-redux';
import { addTodo } from '../actions';
import NewTodo from '../components/NewTodo';

const mapDispatchToProps = dispatch => {
  return {
    onAddTodo: todo => {
      dispatch(addTodo(toto));
    }
  };
};

export default connect(
  null,
  mapDispatchToProps
)(NewTodo);

В самом экшене дело обстоит намного интереснее. Здесь мы будем использовать библиотеку Axios, для ajax запросов. Если она у вас не установлена, то добавьте ее так:

# Yarn
$ yarn add axios

# npm
$ npm install axios --save

Мы будем делать POST запрос на адрес — jsonplaceholder.typicode.com/todos:
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 action creator возвращает функцию, вместо обычного экшен объекта. Эта функция принимает аргумент dispatch из store.

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

Когда мы используем API, который действительно является внешним (удаленным), как JSONPlaceholder в нашем случае, легко заметить что происходит задержка, пока ответ от сервера не приходит. Но если вы работаете с локальным сервером, ответ может приходить слишком быстро, так что вы не заметите задержки. Так-что для своего удобства, вы можете добавить искусственную задержку при разработке:

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));
      });
  };
};

А для тестирования сценария с ошибкой, вы можете напрямую выбросить ошибку:

actions/index.js (кусок кода)

export const addTodo = ({ title, userId }) => {
  return dispatch => {
    dispatch(addTodoStarted());

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

Для полноты картины, вот пример, как наш todo редюсер может выглядеть, что-бы обрабатывать полный «жизненный цикл» запроса:

reducers/todoReducer.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


Функция, возвращаемая асинхронным action creator'ом с помощью Redux-Thunk, также принимает getState метод как второй аргумент, что позволяет получать стейт прямо внутри action creator'а:

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 элементов до 4, мы можем просто выйти из функции, если этот лимит превышается:

actions/index.js (кусок кода)

export const addTodo = ({ title, userId }) => {
  return (dispatch, getState) => {
    const { todos } = getState();

    if (todos.length >= 4) return;

    dispatch(addTodoStarted());

    // ...
  };
};
Забавный факт — а вы знали что код Redux-Thunk состоит только из 14 строк? Можете проверить сами, как Redux-Thunk middleware работает под капотом
Ссылка на оригинал статьи — Asynchronous Redux Actions Using Redux Thunk.
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 6

    0
    MobX? Не, не слышали да?
      +1
      Слышать то слышали но статья новичкам будет полезна
        +1
        А это универсальная истина, что mobx лучше redux? Просто мне казалось, что если у тебя что то и правда сложное, что возможно сломается, то redux проще отлаживать за счёт того, что там нет внутри магии никакой. Нет?
          +1
          В MobX нет ни какой магии, в 4 версии геттеры и сеттеры, в 5 версии Proxy. В Vue тоже самое. Отлаживать элементарно, вы же знаете в какой переменной что-то не так, VS code показывает где к ней обращаются по всему проекту, Jetbrains Webstorm/PHPStorm тоже (правой кнопкой на переменную/функцию и т.п. > Find Usages). Какие могут быть проблемы с отладкой? Вы знаете где что изменяется, и где что читается.
          И да, это универсальная истина, MobX лучше Redux абсолютно во всем.

          P.S. Если вы используете MobX с Dependency Injection, то у меня для вас плохие новости)
            0
            А как вы мокаете зависимости в тестах? Через monkey patch импортов? Передача зависимостей в конструктор стора упрощает юнит-тестирование и явно даёт понять, что требуется стору для функционирования. Если вы про DI-контейнер, то конечно же без него можно обойтись и прописывать зависимости ручками.
              0
              ты всё на глобальных пишешь?)

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое