Pull to refresh

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

Reading time5 min
Views98K
Original author: Alligator.io

Приветствую Хабр! Представляю вашему вниманию перевод статьи — 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.
Tags:
Hubs:
+3
Comments6

Articles

Change theme settings