Избавляемся от генерации шаблонного кода при помощи redux-actions-async

Организация асинхронных экшенов в вашем Redux приложении на примере использования NASA API

Anton Matrenin
4 min readFeb 4, 2018

Связка React-Redux с момента создания “славилась” большим количеством кода, который нужно копировать от одного модуля к другому. Каждая команда борется с этим по-своему либо не борется вовсе.

При беглом осмотре я заметил большую активность в топиках по обсуждению данной темы: https://github.com/reactjs/redux/issues/2295. Есть даже “официальное” частичное решение проблемы, описанное здесь: https://github.com/reactjs/redux/blob/master/docs/recipes/ReducingBoilerplate.md

При использовании шаблонного кода можно выделить такие преимущества:

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

Но при этом есть и существенные недостатки:

  • отсутствие одного стиля — каждый программист будет писать модуль так, как он видит;
  • большая вероятность допустить ошибку при копировании;
  • возможность мутации состояния неопытными разработчиками;
  • дополнительные затраты на тестирование шаблонного кода.

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

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

Этап 1: Определяем тип экшена

Этот этап ничем не отличается от классического подхода создания экшенов в Redux. Подробнее о классическом подходе можно найти в официальной документации: https://redux.js.org/docs/basics/Actions.html#actions

Для примера создадим новый экшн-тип для получения данных с сервера:

// file: actionTypes/nasaActionType.jsexport const NASA_DATA_FETCH = 'NASA_DATA_FETCH';

Этап 2: Создаем функцию для обращения к серверу

В качестве примера возьмем открытое API от NASA для получения данных о каком-либо факте. На этом этапе мы увидим обычную функцию с использованием axios или fetch, которую мы регулярно используем в работе.

// file: endpoints/nasa.jsimport axios from 'axios';

export const fetchPlanet = () =>
axios.request({
url:
'https://api.nasa.gov/planetary/apod?api_key=NNKOjkoul8n1CH18TWA9gwngW1s1SmjESPjNoUFo',
method: 'get',
});

Этап 3: Создаем наш первый асинхронный экшн

Третий этап посвящен написанию action-creator, который будет генерировать 3 экшена:

  • информирование стора о начале запроса (NASA_DATA_FETCH_REQUEST)
  • информирование стора об успешном запросе (NASA_DATA_FETCH_SUCCESS)
  • информирование стора о какой-либо ошибке (NASA_DATA_FETCH_FAIL)

Очевидно, что эту генерацию мы можем упаковать в одну функцию createAsyncAction:

// file: /actionCreators/nasa.jsimport { fetchPlanet } from '../endpoints/nasa';
import { createAsyncAction } from 'redux-actions-async';
import { NASA_DATA_FETCH } from '../actionTypes/nasaActionType';

export const nasaFetchPlanet = createAsyncAction(
NASA_DATA_FETCH,
fetchPlanet
);

В коде мы использовали action-type из этапа 1 и функцию-запрос из этапа 2. В результате мы имеем action-creator, который можем прокинуть в любой контейнер:

import * as actionCreators from './actionCreators/nasa';const mapDispatchToProps = {
nasaFetchPlanet: actionCreators.nasaFetchPlanet,
};

Этап 4: Обработка экшенов в редьюсере

Последний этап направлен на обработку сгенерированных экшенов для использования загруженных данных в наших контейнерах и компонентах. Для этого воспользуемся функцией handleAsyncActions, которая обработает все три экшена, сгенерированные функцией createAsyncAction:

import { handleAsyncActions } from 'redux-actions-async';
import { NASA_DATA_FETCH } from '../actionCreators/nasaActionCreators';

export default handleAsyncActions(NASA_DATA_FETCH);

Что получилось?

Теперь мы можем использовать наш стор для вывода данных на экран, используя React-компоненты:

class Planet extends Component {
componentWillMount() {
this.props.nasaFetchPlanet();
}

render() {
const { loading, planet } = this.props;

if (loading || !planet) {
return <span>Loading...</span>;
}

return (
<div>
<h1>{planet.title}</h1>
<img src={planet.url} width="500" />
</div>
);
}
}

const mapStateToProps = state => ({
loading: state.loading,
planet: state.data,
});

const mapDispatchToProps = {
nasaFetchPlanet: actionCreators.nasaFetchPlanet,
};

export default connect(mapStateToProps, mapDispatchToProps)(Planet);

Результат не заставит нас ждать:

Итоги

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

В следующих статьях мы рассмотрим другие проблемы, которые может решить этот подход создания экшенов:

  • использование normalizr при работе с большим количеством повторяемых объектов;
  • быстрое создание пагинации;
  • единый источник обработки ошибок;
  • отслеживание спинера на странице для нескольких источников данных.

Ссылка на библиотеку:

Ссылка на демо-проект:

--

--