Перейти к основному содержимому
Неофициальный Бета-перевод

Эта страница переведена PageTurner AI (бета). Не одобрена официально проектом. Нашли ошибку? Сообщить о проблеме →

Настройка хранилища

В учебном пособии «Redux Fundamentals» мы рассмотрели основные концепции Redux на примере создания приложения для списка задач. В частности, мы объяснили как создавать и настраивать хранилище Redux.

Теперь мы рассмотрим, как кастомизировать хранилище для добавления дополнительной функциональности. Мы начнём с исходного кода из части 5 «Redux Fundamentals»: UI и React. Вы можете просмотреть исходный код этого этапа в репозитории примера приложения на GitHub или в браузере через CodeSandbox.

Создание хранилища

Сначала рассмотрим исходный файл index.js, где мы создавали хранилище:

import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import rootReducer from './reducers'
import App from './components/App'

const store = createStore(rootReducer)

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

В этом коде мы передаём наши редьюсеры в функцию Redux createStore, которая возвращает объект store. Затем мы передаём этот объект в компонент Provider из react-redux, который рендерится на верхнем уровне нашего дерева компонентов.

Это гарантирует, что при подключении к Redux через connect из react-redux, хранилище будет доступно нашим компонентам.

Расширение функциональности Redux

Большинство приложений расширяют функциональность хранилища Redux, добавляя middleware или усилители (store enhancers) (примечание: middleware используются часто, усилители — реже). Middleware добавляет дополнительную функциональность к функции dispatch в Redux, а усилители расширяют возможности самого хранилища Redux.

Мы добавим два middleware и один усилитель:

  • Middleware redux-thunk, который позволяет выполнять асинхронные операции через dispatch.

  • Middleware для логирования диспатчимых действий и результирующего состояния.

  • Усилитель для логирования времени обработки каждого действия редьюсерами.

Установка redux-thunk

npm install redux-thunk

middleware/logger.js

const logger = store => next => action => {
console.group(action.type)
console.info('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
console.groupEnd()
return result
}

export default logger

enhancers/monitorReducer.js

const round = number => Math.round(number * 100) / 100

const monitorReducerEnhancer =
createStore => (reducer, initialState, enhancer) => {
const monitoredReducer = (state, action) => {
const start = performance.now()
const newState = reducer(state, action)
const end = performance.now()
const diff = round(end - start)

console.log('reducer process time:', diff)

return newState
}

return createStore(monitoredReducer, initialState, enhancer)
}

export default monitorReducerEnhancer

Добавим это в наш существующий index.js.

  • Сначала нам нужно импортировать redux-thunk, а также наши loggerMiddleware и monitorReducerEnhancer, плюс две дополнительные функции из Redux: applyMiddleware и compose.

  • Затем используем applyMiddleware для создания усилителя хранилища, который применит наш loggerMiddleware и thunk middleware к функции dispatch.

  • Далее используем compose для композиции нового middlewareEnhancer и нашего monitorReducerEnhancer в одну функцию.

    Это необходимо потому, что в createStore можно передать только один усилитель. Для использования нескольких усилителей их нужно сначала скомпоновать в один более крупный усилитель, как показано в этом примере.

  • Наконец, передаём эту новую функцию composedEnhancers в createStore в качестве третьего аргумента. Примечание: второй аргумент, который мы проигнорируем, позволяет предзагрузить состояние в хранилище.

import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import { applyMiddleware, createStore, compose } from 'redux'
import { thunk } from 'redux-thunk'
import rootReducer from './reducers'
import loggerMiddleware from './middleware/logger'
import monitorReducerEnhancer from './enhancers/monitorReducer'
import App from './components/App'

const middlewareEnhancer = applyMiddleware(loggerMiddleware, thunk)
const composedEnhancers = compose(middlewareEnhancer, monitorReducerEnhancer)

const store = createStore(rootReducer, undefined, composedEnhancers)

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

Проблемы этого подхода

Хотя этот код работает, для типичного приложения он не идеален.

Большинство приложений используют более одного middleware, и каждому часто требуется начальная настройка. Дополнительный код в index.js может быстро усложнить поддержку, поскольку логика не организована должным образом.

Решение: configureStore

Решение этой проблемы — создание функции configureStore, которая инкапсулирует логику создания хранилища. Её можно вынести в отдельный файл для упрощения расширения.

Цель — привести index.js к следующему виду:

import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import App from './components/App'
import configureStore from './configureStore'

const store = configureStore()

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

Вся логика по настройке хранилища — импорт редьюсеров, middleware и усилителей — вынесена в отдельный файл.

Для реализации функция configureStore будет выглядеть так:

import { applyMiddleware, compose, createStore } from 'redux'
import { thunk } from 'redux-thunk'

import monitorReducersEnhancer from './enhancers/monitorReducers'
import loggerMiddleware from './middleware/logger'
import rootReducer from './reducers'

export default function configureStore(preloadedState) {
const middlewares = [loggerMiddleware, thunk]
const middlewareEnhancer = applyMiddleware(...middlewares)

const enhancers = [middlewareEnhancer, monitorReducersEnhancer]
const composedEnhancers = compose(...enhancers)

const store = createStore(rootReducer, preloadedState, composedEnhancers)

return store
}

Эта функция повторяет ранее описанные шаги, но с разделением логики для подготовки к расширению:

  • middlewares и enhancers определены как массивы, отдельно от функций, которые их используют.

    Это упрощает добавление новых middleware или усилителей в зависимости от условий.

    Например, часто middleware добавляют только в режиме разработки, что легко реализовать через условное добавление в массив:

    if (process.env.NODE_ENV === 'development') {
    middlewares.push(secretMiddleware)
    }
  • Переменная preloadedState передаётся в createStore для возможного будущего использования.

Это также делает функцию createStore более понятной: каждый шаг чётко разделён, что повышает читаемость.

Интеграция расширения для разработки

Ещё одна полезная функция — интеграция расширения redux-devtools-extension.

Этот набор инструментов даёт полный контроль над хранилищем Redux: позволяет инспектировать и воспроизводить действия, исследовать состояние в разные моменты времени, диспетчеризовать действия напрямую и многое другое. Подробнее о возможностях.

Существует несколько способов интеграции, но мы используем самый удобный.

Сначала устанавливаем пакет через npm:

npm install --save-dev redux-devtools-extension

Затем заменяем импортированную из redux функцию compose на composeWithDevTools из redux-devtools-extension.

Итоговый код:

import { applyMiddleware, createStore } from 'redux'
import { thunk } from 'redux-thunk'
import { composeWithDevTools } from 'redux-devtools-extension'

import monitorReducersEnhancer from './enhancers/monitorReducers'
import loggerMiddleware from './middleware/logger'
import rootReducer from './reducers'

export default function configureStore(preloadedState) {
const middlewares = [loggerMiddleware, thunk]
const middlewareEnhancer = applyMiddleware(...middlewares)

const enhancers = [middlewareEnhancer, monitorReducersEnhancer]
const composedEnhancers = composeWithDevTools(...enhancers)

const store = createStore(rootReducer, preloadedState, composedEnhancers)

return store
}

Готово!

Если открыть приложение в браузере с установленным расширением, мы получим мощный инструмент для отладки.

Горячая перезагрузка

Ещё один мощный инструмент, ускоряющий разработку, — горячая перезагрузка (hot reloading), позволяющая заменять части кода без перезапуска приложения.

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

С горячей перезагрузкой обновится только изменённый редьюсер, что позволяет вносить правки без сброса состояния. Это ускоряет разработку.

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

Сначала интегрируем в функцию configureStore:

import { applyMiddleware, compose, createStore } from 'redux'
import { thunk } from 'redux-thunk'

import monitorReducersEnhancer from './enhancers/monitorReducers'
import loggerMiddleware from './middleware/logger'
import rootReducer from './reducers'

export default function configureStore(preloadedState) {
const middlewares = [loggerMiddleware, thunk]
const middlewareEnhancer = applyMiddleware(...middlewares)

const enhancers = [middlewareEnhancer, monitorReducersEnhancer]
const composedEnhancers = compose(...enhancers)

const store = createStore(rootReducer, preloadedState, composedEnhancers)

if (process.env.NODE_ENV !== 'production' && module.hot) {
module.hot.accept('./reducers', () => store.replaceReducer(rootReducer))
}

return store
}

Новый код обёрнут в if, поэтому выполняется только вне production-режима и при поддержке module.hot.

Сборщики вроде Webpack и Parcel поддерживают метод module.hot.accept для указания модулей, требующих горячей перезагрузки. Здесь мы отслеживаем изменения в ./reducers и передаём обновлённый rootReducer в store.replaceReducer.

Аналогичный подход применим в index.js для горячей перезагрузки компонентов React:

import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import App from './components/App'
import configureStore from './configureStore'

const store = configureStore()

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

if (process.env.NODE_ENV !== 'production' && module.hot) {
module.hot.accept('./components/App', renderApp)
}

renderApp()

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

Упрощение настройки с помощью Redux Toolkit

Библиотека Redux сознательно не навязывает конкретных решений. Она позволяет вам самостоятельно решать, как обрабатывать всё: настройку хранилища, структуру состояния и построение редюсеров.

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

Пакет Redux Toolkit создан для упрощения распространённых сценариев использования Redux, включая настройку хранилища. Посмотрим, как он помогает улучшить этот процесс.

Redux Toolkit включает предустановленную функцию configureStore, аналогичную показанной в предыдущих примерах.

Самый быстрый способ использования — просто передать корневой редюсер:

import { configureStore } from '@reduxjs/toolkit'
import rootReducer from './reducers'

const store = configureStore({
reducer: rootReducer
})

export default store

Обратите внимание, что функция принимает объект с именованными параметрами для ясности передаваемых значений.

По умолчанию configureStore из Redux Toolkit:

  • Вызывает applyMiddleware со стандартным набором middleware, включая redux-thunk, и дополнительными middleware для разработки, которые отлавливают типичные ошибки (например, мутацию состояния)

  • Использует composeWithDevTools для подключения Redux DevTools Extension

Вот как может выглядеть пример с горячей перезагрузкой при использовании Redux Toolkit:

import { configureStore } from '@reduxjs/toolkit'

import monitorReducersEnhancer from './enhancers/monitorReducers'
import loggerMiddleware from './middleware/logger'
import rootReducer from './reducers'

export default function configureAppStore(preloadedState) {
const store = configureStore({
reducer: rootReducer,
middleware: getDefaultMiddleware =>
getDefaultMiddleware().prepend(loggerMiddleware),
preloadedState,
enhancers: [monitorReducersEnhancer]
})

if (process.env.NODE_ENV !== 'production' && module.hot) {
module.hot.accept('./reducers', () => store.replaceReducer(rootReducer))
}

return store
}

Это определённо упрощает процесс настройки.

Следующие шаги

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