В настоящее время львиная доля веб-приложений на базе React разрабатывается с использованием библиотеки Redux. Данная библиотека является наиболее популярной реализацией архитектуры FLUX и, несмотря на ряд очевидных преимуществ, имеет весьма существенные недостатки, такие как:
- сложность и «многословность» рекомендуемых шаблонов написания и организации кода, что влечет за собой большое количество шаблонного кода;
- отсутствие встроенных средств контроля асинхронного поведения и побочных эффектов, что приводит к необходимости выбора подходящего инструмента из множества дополнений, написанных сторонними разработчиками.
Этот инструмент представляет собой набор практических решений и методов, призванных упростить разработку приложений с использованием Redux.
Разработчики этой библиотеки стремились упростить типичные случаи использования Redux.
Этот инструмент не является универсальным решением для всех возможных вариантов использования Redux, но он позволяет упростить код, который необходимо написать разработчику.
В этой статье мы поговорим об основных инструментах, входящих в Redux Toolkit, а также на примере фрагмента нашего внутреннего приложения покажем, как их использовать в существующем коде.
Коротко о библиотеке
Краткая информация о Redux Toolkit:- до выпуска библиотека называлась redux-starter-kit;
- релиз состоялся в конце октября 2019 года;
- Библиотека официально поддерживается разработчиками Redux.
- помогает быстро начать использовать Redux;
- упрощает работу с типовыми задачами и кодом Redux;
- позволяет использовать лучшие практики Redux по умолчанию;
- предлагает решения, которые уменьшают недоверие к шаблонам.
По мере развития этой статьи мы будем отмечать, какие заимствования использует эта библиотека.
Более полную информацию и зависимости Redux Toolkit можно получить из описания пакета.
Наиболее важные функции, предоставляемые библиотекой Redux Toolkit:
- #configureStore — функция, призванная упростить процесс создания и настройки хранилища;
- #createReducer — функция, помогающая кратко и понятно описать и создать редюсер;
- #createAction — возвращает функцию создателя действия для заданной строки типа действия;
- #createSlice — сочетает в себе функциональность createAction и createReducer;
- createSelector — функция из библиотеки Повторно выбрать , реэкспортирован для удобства использования.
Приложение
Давайте рассмотрим использование библиотеки Redux Toolkit на примере фрагмента реально используемого приложения React Redux. Примечание .Далее в статье будет предоставлен исходный код как без использования Redux Toolkit, так и с его использованием, что позволит вам лучше оценить положительные и отрицательные стороны использования этой библиотеки.
Задача
В одном из наших внутренних приложений возникла необходимость добавления, редактирования и отображения информации о релизах наших программных продуктов.Для каждого из этих действий были разработаны отдельные API-функции, результаты выполнения которых необходимо добавить в хранилище Redux. В качестве средства контроля асинхронного поведения и побочных эффектов мы будем использовать Thunk .
Создание репозитория
Первоначальная версия исходного кода, создавшего репозиторий, выглядела так: import {
createStore, applyMiddleware, combineReducers, compose,
} from 'redux';
import thunk from 'redux-thunk';
import * as reducers from '.
/reducers';
const ext = window.__REDUX_DEVTOOLS_EXTENSION__;
const devtoolMiddleware =
ext && process.env.NODE_ENV === 'development' ? ext() : f => f;
const store = createStore(
combineReducers({
.
reducers,
}),
compose(
applyMiddleware(thunk),
devtoolMiddleware
)
);
Если внимательно посмотреть на приведенный выше код, то можно увидеть довольно длинную последовательность действий, которые необходимо выполнить, чтобы хранилище было полностью настроено.
Redux Toolkit содержит инструмент, предназначенный для упрощения этой процедуры, а именно функцию configureStore.
функция configureStore
Этот инструмент позволяет автоматически комбинировать редукторы, добавлять промежуточное программное обеспечение Redux (по умолчанию включает redux-thunk), а также использовать расширение Redux DevTools. Функция configureStore принимает на вход объект со следующими свойствами:- редуктор — набор кастомных редукторов,
- middleware — необязательный параметр, задающий массив промежуточного программного обеспечения, предназначенного для подключения к хранилищу,
- devTools — логический параметр, позволяющий включить расширение Redux DevTools, установленное в браузере (значение по умолчанию — true),
- preloadedState — необязательный параметр, задающий начальное состояние хранилища,
- Enhancers — необязательный параметр, задающий набор усилителей.
Эта функция возвращает массив с промежуточным программным обеспечением, включенным по умолчанию в библиотеку Redux Toolkit.
Список этих промежуточных программ различается в зависимости от того, в каком режиме работает ваш код.
В производственном режиме массив состоит только из одного элемента — thunk.
В режиме разработки на момент написания статьи список пополняется следующими промежуточными программами:
- SerializableStateInvariant — инструмент, специально разработанный для использования в Redux Toolkit и предназначенный для проверки дерева состояний на наличие несериализуемых значений, таких как функции, Promise, Символ и других значений, которые не являются простыми данными JS;
- immutableStateInvariant — промежуточное ПО из пакета инвариант-неизменяемого-состояния , предназначенный для обнаружения мутаций в данных, содержащихся в хранилище.
Более подробную информацию об этой информации можно найти в соответствующем раздел официальная документация.
Теперь перепишем участок кода, отвечающий за создание хранилища, используя описанные выше инструменты.
В результате мы получаем следующее: import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit';
import * as reducers from '.
/reducers'; const middleware = getDefaultMiddleware({ immutableCheck: false, serializableCheck: false, thunk: true, }); export const store = configureStore({ reducer: { .
reducers }, middleware, devTools: process.env.NODE_ENV !== 'production', }); На примере этого фрагмента кода хорошо видно, что функция configureStore решает следующие проблемы:
- необходимость объединять редюсеры путем автоматического вызова mergeReducers,
- необходимость объединения промежуточного программного обеспечения путем автоматического вызова applyMiddleware.
Все вышесказанное говорит о том, что использование данной функции позволяет сделать код более компактным и понятным.
На этом создание и настройка хранилища завершена.
Передаем его провайдеру и идем дальше.
Действия, создатели действий и редуктор
Теперь давайте посмотрим на возможности Redux Toolkit с точки зрения разработки действий, создателей действий и редьюсера.Первоначальная версия кода без использования Redux Toolkit была организована в виде файлов action.js и редукторов.
js. Содержимое файла action.js выглядело следующим образом: import * as productReleasesService from '.
/.
/services/productReleases';
export const PRODUCT_RELEASES_FETCHING = 'PRODUCT_RELEASES_FETCHING';
export const PRODUCT_RELEASES_FETCHED = 'PRODUCT_RELEASES_FETCHED';
export const PRODUCT_RELEASES_FETCHING_ERROR =
'PRODUCT_RELEASES_FETCHING_ERROR';
…
export const PRODUCT_RELEASE_UPDATING = 'PRODUCT_RELEASE_UPDATING';
export const PRODUCT_RELEASE_UPDATED = 'PRODUCT_RELEASE_UPDATED';
export const PRODUCT_RELEASE_CREATING_UPDATING_ERROR =
'PRODUCT_RELEASE_CREATING_UPDATING_ERROR';
function productReleasesFetching() {
return {
type: PRODUCT_RELEASES_FETCHING
};
}
function productReleasesFetched(productReleases) {
return {
type: PRODUCT_RELEASES_FETCHED,
productReleases
};
}
function productReleasesFetchingError(error) {
return {
type: PRODUCT_RELEASES_FETCHING_ERROR,
error
}
}
…
export function fetchProductReleases() {
return dispatch => {
dispatch(productReleasesFetching());
return productReleasesService.getProductReleases().
then( productReleases => dispatch(productReleasesFetched(productReleases)) ).
catch(error => {
error.clientMessage = "Can't get product releases";
dispatch(productReleasesFetchingError(error))
});
}
}
…
export function updateProductRelease(
id, productName, productVersion, releaseDate
) {
return dispatch => {
dispatch(productReleaseUpdating());
return productReleasesService.updateProductRelease(
id, productName, productVersion, releaseDate
).
then( productRelease => dispatch(productReleaseUpdated(productRelease)) ).
catch(error => { error.clientMessage = "Can't update product releases"; dispatch(productReleaseCreatingUpdatingError(error)) }); } } Содержимое файла редукторов.
js перед использованием Redux Toolkit: const initialState = {
productReleases: [],
loadedProductRelease: null,
fetchingState: 'none',
creatingState: 'none',
updatingState: 'none',
error: null,
};
export default function reducer(state = initialState, action = {}) {
switch (action.type) {
case productReleases.PRODUCT_RELEASES_FETCHING:
return {
.
state, fetchingState: 'requesting', error: null, }; case productReleases.PRODUCT_RELEASES_FETCHED: return { .
state, productReleases: action.productReleases, fetchingState: 'success', }; case productReleases.PRODUCT_RELEASES_FETCHING_ERROR: return { .
state, fetchingState: 'failed', error: action.error }; … case productReleases.PRODUCT_RELEASE_UPDATING: return { .
state, updatingState: 'requesting', error: null, }; case productReleases.PRODUCT_RELEASE_UPDATED: return { .
state,
updatingState: 'success',
productReleases: state.productReleases.map(productRelease => {
if (productRelease.id === action.productRelease.id)
return action.productRelease;
return productRelease;
})
};
case productReleases.PRODUCT_RELEASE_UPDATING_ERROR:
return {
.
state,
updatingState: 'failed',
error: action.error
};
default:
return state;
}
} Как мы видим, именно здесь содержится большая часть шаблона: константы типов действий, создатели действий, снова константы, но в коде редюсера приходится тратить время на написание всего этого кода.
Частично избавиться от этого шаблона можно, если использовать функции createAction и createReducer, которые также включены в Redux Toolkit.
функция createAction
В приведенном выше участке кода используется стандартный способ определения действия в Redux: сначала отдельно объявляется константа, определяющая тип действия, после чего объявляется функция создателя действия этого типа.Функция создать действие объединяет эти два объявления в одно.
Он принимает тип действия в качестве входных данных и возвращает создателя действия для этого типа.
Создатель действия может быть вызван либо без аргументов, либо с каким-либо аргументом (полезной нагрузкой), значение которого будет помещено в поле полезной нагрузки создаваемого действия.
Кроме того, создатель действия переопределяет функцию toString(), чтобы тип действия стал его строковым представлением.
В некоторых случаях вам может потребоваться написать дополнительную логику для настройки значения полезной нагрузки, например принятие нескольких параметров для создателя действия, создание случайного идентификатора или получение текущей метки времени.
Для этого createAction принимает необязательный второй аргумент — функцию, которая будет использоваться для обновления значения полезной нагрузки.
Подробнее об этом параметр можно найти в официальной документации.
Используя функцию createAction, мы получаем следующий код: export const productReleasesFetching =
createAction('PRODUCT_RELEASES_FETCHING');
export const productReleasesFetched =
createAction('PRODUCT_RELEASES_FETCHED');
export const productReleasesFetchingError =
createAction('PRODUCT_RELEASES_FETCHING_ERROR');
…
export function fetchProductReleases() {
return dispatch => {
dispatch(productReleasesFetching());
return productReleasesService.getProductReleases().
then( productReleases => dispatch(productReleasesFetched({ productReleases })) ).
catch(error => { error.clientMessage = "Can't get product releases"; dispatch(productReleasesFetchingError({ error })) }); } } .
функция createReducer
Теперь посмотрим на редуктор.Как и в нашем примере, редукторы часто реализуются с использованием оператора переключения, при этом для каждого типа обрабатываемого действия используется один регистр.
Этот подход работает хорошо, но не лишен шаблонного подхода и подвержен ошибкам.
Например, легко забыть описать случай по умолчанию или не установить начальное состояние.
Функция createReducer упрощает создание функций редуктора, определяя их как таблицы поиска функций для обработки каждого типа действий.
Это также позволяет значительно упростить логику неизменяемого обновления, написав код в «изменяемом» стиле внутри редьюсеров.
«Изменяемый» стиль обработки событий доступен с помощью библиотеки погружаться .
Функция-обработчик может либо «мутировать» переданное состояние для изменения свойств, либо возвращать новое состояние, как при работе в неизменяемом стиле, но благодаря Immer фактическая мутация объекта не осуществляется.
С первым вариантом гораздо проще работать и понимать, особенно при изменении глубоко вложенного объекта.
Будьте осторожны: возврат нового объекта из функции отменяет «изменяемые» изменения.
Одновременное использование обоих методов обновления состояния не сработает. Функция createReducer принимает в качестве входных параметров следующие аргументы:
- исходное состояние хранилища,
- объект, устанавливающий соответствие между типами действий и редьюсерами, каждый из которых обрабатывает определенный тип.
const initialState = {
productReleases: [],
loadedProductRelease: null,
fetchingState: 'none',
creatingState: 'none',
loadingState: 'none',
error: null,
};
const counterReducer = createReducer(initialState, {
[productReleasesFetching]: (state, action) => {
state.fetchingState = 'requesting'
},
[productReleasesFetched.type]: (state, action) => {
state.productReleases = action.payload.productReleases;
state.fetchingState = 'success';
},
[productReleasesFetchingError]: (state, action) => {
state.fetchingState = 'failed';
state.error = action.payload.error;
},
…
[productReleaseUpdating]: (state) => {
state.updatingState = 'requesting'
},
[productReleaseUpdated]: (state, action) => {
state.updatingState = 'success';
state.productReleases = state.productReleases.map(productRelease => {
if (productRelease.id === action.payload.productRelease.id)
return action.payload.productRelease;
return productRelease;
});
},
[productReleaseUpdatingError]: (state, action) => {
state.updating = 'failed';
state.error = action.payload.error;
},
});
Как мы видим, использование функций createAction и createReducer существенно решает проблему написания ненужного кода, но проблема предварительного создания констант все равно остается.
Поэтому давайте рассмотрим более мощный вариант, сочетающий в себе создатели генерации и действий и редуктор — функцию createSlice.
функция createSlice
Функция createSlice принимает на вход объект со следующими полями:- name — пространство имен созданных действий (
${name}/${action.type}
); - InitialState — начальное состояние редюсера;
- редукторы — объект с обработчиками.
Каждый обработчик принимает функцию с аргументами состояния и действия, действие содержит данные в свойстве полезной нагрузки и имя события в свойстве имени.
Кроме того, есть возможность предварительно модифицировать полученные от события данные до того, как они попадут в редьюсер (например, добавить id к элементам коллекции).
Для этого вместо функции необходимо передать объект с полями редуктора и подготовки, где редуктор — это функция-обработчик действий, а подготовка — функция-обработчик полезной нагрузки, которая возвращает обновленную полезную нагрузку;
- extraReducers — это объект, содержащий редукторы другого среза.
Этот параметр может потребоваться, если необходимо обновить объект, принадлежащий другому срезу.
Подробнее об этом функционале можно прочитать в соответствующем разделе.
раздел официальная документация.
- name — имя слайса,
- редуктор - редуктор,
- действия — набор действий.
const initialState = {
productReleases: [],
loadedProductRelease: null,
fetchingState: 'none',
creatingState: 'none',
loadingState: 'none',
error: null,
};
const productReleases = createSlice({
name: 'productReleases',
initialState,
reducers: {
productReleasesFetching: (state) => {
state.fetchingState = 'requesting';
},
productReleasesFetched: (state, action) => {
state.productReleases = action.payload.productReleases;
state.fetchingState = 'success';
},
productReleasesFetchingError: (state, action) => {
state.fetchingState = 'failed';
state.error = action.payload.error;
},
…
productReleaseUpdating: (state) => {
state.updatingState = 'requesting'
},
productReleaseUpdated: (state, action) => {
state.updatingState = 'success';
state.productReleases = state.productReleases.map(productRelease => {
if (productRelease.id === action.payload.productRelease.id)
return action.payload.productRelease;
return productRelease;
});
},
productReleaseUpdatingError: (state, action) => {
state.updating = 'failed';
state.error = action.payload.error;
},
},
});
Теперь давайте извлечем создателей действий и редуктор из созданного фрагмента.
const { actions, reducer } = productReleases;
export const {
productReleasesFetched, productReleasesFetching,
productReleasesFetchingError,
…
productReleaseUpdated,
productReleaseUpdating, productReleaseUpdatingError
} = actions;
export default reducer;
Исходный код создателей действий, содержащих вызовы API, не изменился, за исключением способа передачи параметров при отправке действий: export const fetchProductReleases = () => (dispatch) => {
dispatch(productReleasesFetching());
return productReleasesService
.
getProductReleases() .
then((productReleases) => dispatch(productReleasesFetched({ productReleases }))) .
catch((error) => {
error.clientMessage = "Can't get product releases";
dispatch(productReleasesFetchingError({ error }));
});
};
…
export const updateProductRelease = (id, productName, productVersion, releaseDate) => (dispatch) => {
dispatch(productReleaseUpdating());
return productReleasesService
.
updateProductRelease(id, productName, productVersion, releaseDate) .
then((productRelease) => dispatch(productReleaseUpdated({ productRelease }))) .
catch((error) => {
error.clientMessage = "Can't update product releases";
dispatch(productReleaseUpdatingError({ error }));
}); Приведенный выше код показывает, что функция createSlice позволяет избавиться от значительной части шаблонности при работе с Redux, что позволяет не только сделать код более компактным, лаконичным и понятным, но и тратить меньше времени на его написание.
Нижняя граница
В конце этой статьи хотелось бы сказать, что несмотря на то, что библиотека Redux Toolkit не привносит ничего нового в управление хранилищем, она предоставляет ряд гораздо более удобных инструментов для написания кода, чем раньше.Эти инструменты позволяют не только сделать процесс разработки более удобным, понятным и быстрым, но и более эффективным, благодаря наличию в библиотеке ряда ранее хорошо зарекомендовавших себя инструментов.
Мы в «Инобитек» планируем и дальше использовать эту библиотеку при разработке наших программных продуктов и следить за новыми перспективными разработками в области веб-технологий.
Спасибо за внимание.
Надеемся, что наша статья будет полезна.
Более подробную информацию о библиотеке Redux Toolkit можно получить на официальном сайте.
Теги: #Хранилища данных #Хранилище данных #Разработка веб-сайтов #JavaScript #frontend #front-end development #хранилище данных #flux #react.js #typescript #react #toolkit #redux #Immer #redux-thunk #state #redux middleware #Web Разработка #Store #redux-utils #redux-dev-tools #redux-toolkit #reduxtoolkit #redux-starter-kit #starter-kit #reduxjs #starters kit #flux and React #reducer #action #reselect #redux-create -reducer #redux-create-action #redux-immutable-state-invariant
-
Управляйте Торговлей С Хостингом Act Premium
19 Dec, 24 -
Как Определить Объем Ваших Бревен?
19 Dec, 24 -
Примечания О Стиле Кода
19 Dec, 24 -
Полетит Ли Звездолет На Марс?
19 Dec, 24 -
Теннис На Двоих
19 Dec, 24 -
Будущие Выпуски Firefox
19 Dec, 24 -
Подборка Обучающих Слайдов
19 Dec, 24 -
Поиск Методов В Squeak Small Talk
19 Dec, 24