Перейти к основному содержимому

Основы Redux, Часть 4: Хранилище (Store)

Неофициальный Бета-перевод

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

Что вы узнаете
  • Как создать хранилище Redux
  • Как использовать хранилище для обновления состояния и отслеживания изменений
  • Как настроить хранилище для расширения его возможностей
  • Как настроить Redux DevTools Extension для отладки приложения

Введение

В Части 3: Состояние, действия и редюсеры мы начали разрабатывать пример todo-приложения. Мы перечислили бизнес-требования, определили структуру состояния, необходимую для работы приложения, и создали набор типов действий, чтобы описывать "что произошло" в соответствии с событиями, возникающими при взаимодействии пользователя с приложением. Мы также написали редюсеры, обрабатывающие обновления разделов state.todos и state.filters, и увидели, как можно использовать функцию Redux combineReducers для создания "корневого редюсера" на основе отдельных "срезовых редюсеров" для каждой функциональности приложения.

Теперь пришло время объединить эти компоненты вокруг центрального элемента Redux-приложения — хранилища (store).

Неофициальный Бета-перевод

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

Внимание!

Обратите внимание, что в этом руководстве намеренно используются устаревшие шаблоны логики Redux, требующие больше кода, чем "современные Redux" шаблоны с Redux Toolkit, которые мы рекомендуем как правильный подход для создания приложений на Redux сегодня. Это сделано для объяснения принципов и концепций Redux. Данный материал не предназначен для использования в production-проектах.

Чтобы изучить "современный Redux" с Redux Toolkit, перейдите по ссылкам:

Хранилище Redux

Хранилище Redux объединяет состояние, действия и редюсеры, составляющие ваше приложение. На хранилище возложено несколько обязанностей:

  • Содержит текущее состояние приложения

  • Предоставляет доступ к текущему состоянию через store.getState();

  • Позволяет обновлять состояние через store.dispatch(action);

  • Регистрирует функции-обработчики через store.subscribe(listener);

  • Обрабатывает отмену регистрации обработчиков через функцию unsubscribe, возвращаемую store.subscribe(listener).

Важно отметить, что в Redux-приложении будет только одно хранилище. Когда вам нужно разделить логику обработки данных, используйте композицию редюсеров и создайте несколько редюсеров, которые можно объединить, вместо создания отдельных хранилищ.

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

Каждое хранилище Redux имеет единственную корневую функцию-редюсер. В предыдущем разделе мы создали корневой редюсер с помощью combineReducers. Этот корневой редюсер определен в файле src/reducer.js нашего примера приложения. Импортируем его и создадим наше первое хранилище.

Базовая библиотека Redux содержит API createStore для создания хранилища. Создайте новый файл store.js, импортируйте createStore и корневой редюсер. Затем вызовите createStore, передав корневой редюсер:

src/store.js
import { createStore } from 'redux'
import rootReducer from './reducer'

const store = createStore(rootReducer)

export default store

Загрузка начального состояния

createStore также может принимать значение preloadedState в качестве второго аргумента. Это позволяет добавить начальные данные при создании хранилища, например, значения из HTML-страницы, полученной с сервера, или сохранённые в localStorage и прочитанные при повторном посещении страницы пользователем:

storeStatePersistenceExample.js
import { createStore } from 'redux'
import rootReducer from './reducer'

let preloadedState
const persistedTodosString = localStorage.getItem('todos')

if (persistedTodosString) {
preloadedState = {
todos: JSON.parse(persistedTodosString)
}
}

const store = createStore(rootReducer, preloadedState)

Отправка действий (Dispatching Actions)

Теперь, когда хранилище создано, проверим работу программы! Даже без пользовательского интерфейса мы уже можем протестировать логику обновления.

Совет

Прежде чем запускать этот код, вернитесь в src/features/todos/todosSlice.js и удалите все примеры задач из initialState, оставив пустой массив. Это упростит чтение вывода в данном примере.

src/index.js
// Omit existing React imports

import store from './store'

// Log the initial state
console.log('Initial state: ', store.getState())
// {todos: [....], filters: {status, colors}}

// Every time the state changes, log it
// Note that subscribe() returns a function for unregistering the listener
const unsubscribe = store.subscribe(() =>
console.log('State after dispatch: ', store.getState())
)

// Now, dispatch some actions

store.dispatch({ type: 'todos/todoAdded', payload: 'Learn about actions' })
store.dispatch({ type: 'todos/todoAdded', payload: 'Learn about reducers' })
store.dispatch({ type: 'todos/todoAdded', payload: 'Learn about stores' })

store.dispatch({ type: 'todos/todoToggled', payload: 0 })
store.dispatch({ type: 'todos/todoToggled', payload: 1 })

store.dispatch({ type: 'filters/statusFilterChanged', payload: 'Active' })

store.dispatch({
type: 'filters/colorFilterChanged',
payload: { color: 'red', changeType: 'added' }
})

// Stop listening to state updates
unsubscribe()

// Dispatch one more action to see what happens

store.dispatch({ type: 'todos/todoAdded', payload: 'Try creating a store' })

// Omit existing React rendering logic

Помните: каждый раз при вызове store.dispatch(action):

  • Хранилище вызывает rootReducer(state, action)

    • Этот корневой редюсер может вызывать другие редюсеры внутри себя, например todosReducer(state.todos, action)
  • Хранилище сохраняет новое значение состояния внутри себя

  • Хранилище вызывает все callback-функции подписчиков (listeners)

  • Если подписчик имеет доступ к store, он может теперь вызвать store.getState() для чтения актуального состояния

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

Logged Redux state after dispatching actions

Обратите внимание, что наше приложение не вывело ничего для последнего действия. Это потому что мы удалили callback-подписчик при вызове unsubscribe(), поэтому после отправки действия ничего больше не выполнилось.

Мы определили поведение нашего приложения ещё до начала разработки UI. Это помогает убедиться, что приложение будет работать как задумано.

Информация

При желании вы можете написать тесты для своих редюсеров. Поскольку они являются чистыми функциями, их тестирование должно быть простым. Вызывайте их с примером state и action, получайте результат и проверяйте его соответствие ожиданиям:

todosSlice.spec.js
import todosReducer from './todosSlice'

test('Toggles a todo based on id', () => {
const initialState = [{ id: 0, text: 'Test text', completed: false }]

const action = { type: 'todos/todoToggled', payload: 0 }
const result = todosReducer(initialState, action)
expect(result[0].completed).toBe(true)
})

Внутреннее устройство хранилища Redux

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

miniReduxStoreExample.js
function createStore(reducer, preloadedState) {
let state = preloadedState
const listeners = []

function getState() {
return state
}

function subscribe(listener) {
listeners.push(listener)
return function unsubscribe() {
const index = listeners.indexOf(listener)
listeners.splice(index, 1)
}
}

function dispatch(action) {
state = reducer(state, action)
listeners.forEach(listener => listener())
}

dispatch({ type: '@@redux/INIT' })

return { dispatch, subscribe, getState }
}

Эта упрощённая версия хранилища Redux работает достаточно хорошо, чтобы вы могли заменить ею настоящую функцию createStore из Redux, которую использовали до сих пор. (Попробуйте и убедитесь сами!) Реальная реализация хранилища Redux длиннее и сложнее, но большая часть кода — это комментарии, предупреждения и обработка крайних случаев.

Как видите, основная логика довольно лаконична:

  • Хранилище содержит текущее значение state и функцию reducer

  • getState возвращает текущее состояние

  • subscribe хранит массив callback-функций подписчиков и возвращает функцию для удаления новой подписки

  • dispatch вызывает редюсер, сохраняет состояние и запускает подписчиков

  • Хранилище отправляет одно действие при инициализации, чтобы установить начальное состояние для редюсеров

  • API хранилища представляет собой объект {dispatch, subscribe, getState}

Важно подчеркнуть один момент: обратите внимание, что getState просто возвращает текущее значение state. Это означает, что по умолчанию ничто не мешает вам случайно изменить текущее состояние! Этот код выполнится без ошибок, но он некорректен:

const state = store.getState()
// ❌ Don't do this - it mutates the current state!
state.filters.status = 'Active'

Другими словами:

  • Хранилище Redux не создаёт дополнительную копию значения state при вызове getState(). Возвращается именно та ссылка, которую вернул корневой редюсер

  • Хранилище Redux не предпринимает других действий для предотвращения случайных мутаций. Изменение состояния возможно как внутри редюсера, так и вне хранилища, поэтому вы всегда должны избегать мутаций.

Одной из распространённых причин случайных мутаций является сортировка массивов. Вызов array.sort() фактически изменяет исходный массив. Если бы мы вызвали const sortedTodos = state.todos.sort(), мы нечаянно изменили бы реальное состояние хранилища.

Совет

В Части 8: Современный Redux мы увидим, как Redux Toolkit помогает избегать мутаций в редюсерах, а также обнаруживает и предупреждает о случайных мутациях вне их.

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

Мы уже видели, что можем передавать аргументы rootReducer и preloadedState в createStore. Однако createStore также может принимать ещё один аргумент, который используется для настройки возможностей хранилища и расширения его функционала.

Хранилища Redux настраиваются с помощью так называемых расширителей хранилища (store enhancer). Расширитель хранилища — это особая версия createStore, которая добавляет дополнительный слой поверх оригинального хранилища Redux. Расширенное хранилище может изменять своё поведение, предоставляя собственные версии функций dispatch, getState и subscribe вместо оригинальных.

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

Создание хранилища с расширителями

В нашем проекте доступны два небольших примера расширителей хранилища в файле src/exampleAddons/enhancers.js:

  • sayHiOnDispatch: расширитель, который всегда выводит в консоль 'Hi'! при каждом диспатче действия

  • includeMeaningOfLife: расширитель, который всегда добавляет поле meaningOfLife: 42 к значению, возвращаемому из getState()

Начнём с использования sayHiOnDispatch. Сначала импортируем его и передадим в createStore:

src/store.js
import { createStore } from 'redux'
import rootReducer from './reducer'
import { sayHiOnDispatch } from './exampleAddons/enhancers'

const store = createStore(rootReducer, undefined, sayHiOnDispatch)

export default store

У нас нет значения preloadedState, поэтому вместо него передадим undefined в качестве второго аргумента.

Теперь попробуем отправить действие:

src/index.js
import store from './store'

console.log('Dispatching action')
store.dispatch({ type: 'todos/todoAdded', payload: 'Learn about actions' })
console.log('Dispatch complete')

Посмотрите в консоль. Вы должны увидеть 'Hi!' в выводе между двумя другими записями:

sayHi store enhancer logging

Расширитель sayHiOnDispatch обернул оригинальную функцию store.dispatch в свою собственную специализированную версию dispatch. Когда мы вызывали store.dispatch(), фактически мы вызывали обёртку из sayHiOnDispatch, которая вызывала оригинальную функцию, а затем выводила 'Привет!'.

Теперь попробуем добавить второй расширитель. Мы можем импортировать includeMeaningOfLife из того же файла... но возникает проблема. createStore принимает только один расширитель в качестве третьего аргумента! Как же передать два расширителя одновременно?

Нам нужен способ объединить расширители sayHiOnDispatch и includeMeaningOfLife в один комбинированный расширитель, а затем передать именно его.

К счастью, в Redux есть функция compose, которая может объединять несколько расширителей. Воспользуемся ей:

src/store.js
import { createStore, compose } from 'redux'
import rootReducer from './reducer'
import {
sayHiOnDispatch,
includeMeaningOfLife
} from './exampleAddons/enhancers'

const composedEnhancer = compose(sayHiOnDispatch, includeMeaningOfLife)

const store = createStore(rootReducer, undefined, composedEnhancer)

export default store

Теперь посмотрим, что произойдёт при использовании хранилища:

src/index.js
import store from './store'

store.dispatch({ type: 'todos/todoAdded', payload: 'Learn about actions' })
// log: 'Hi!'

console.log('State after dispatch: ', store.getState())
// log: {todos: [...], filters: {status, colors}, meaningOfLife: 42}

Вот как будет выглядеть вывод в консоли:

meaningOfLife store enhancer logging

Как видим, оба расширителя одновременно изменяют поведение хранилища. sayHiOnDispatch изменил работу dispatch, а includeMeaningOfLife изменил поведение getState.

Усилители хранилища (store enhancers) — мощный инструмент для модификации хранилища, и почти все Redux-приложения включают хотя бы один усилитель при настройке.

Совет

Если вам не нужно передавать preloadedState, вы можете передать enhancer вторым аргументом:

const store = createStore(rootReducer, storeEnhancer)

Middleware

Усилители обладают большой гибкостью, поскольку могут переопределять или заменять любые методы хранилища: dispatch, getState и subscribe.

Но часто требуется кастомизировать только поведение dispatch. Было бы удобно иметь способ добавлять пользовательскую логику при выполнении dispatch.

Redux использует специальный тип расширений — middleware — для настройки функции dispatch.

Если вы работали с такими библиотеками, как Express или Koa, концепция middleware для кастомизации поведения может быть вам знакома. В этих фреймворках middleware — это код, который можно поместить между получением запроса фреймворком и генерацией ответа. Например, middleware в Express/Koa может добавлять CORS-заголовки, логирование, сжатие данных и т.д. Главное преимущество middleware — композируемость в цепочку. Вы можете использовать несколько независимых middleware от сторонних разработчиков в одном проекте.

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

Сначала мы рассмотрим добавление middleware в хранилище, затем покажем, как писать собственные реализации.

Использование Middleware

Мы уже знаем, что хранилище Redux можно кастомизировать с помощью усилителей (enhancers). Redux middleware реализованы поверх специального встроенного усилителя — applyMiddleware.

Поскольку мы умеем добавлять усилители в хранилище, применим этот подход. Начнём с applyMiddleware и добавим три примера middleware из этого проекта.

src/store.js
import { createStore, applyMiddleware } from 'redux'
import rootReducer from './reducer'
import { print1, print2, print3 } from './exampleAddons/middleware'

const middlewareEnhancer = applyMiddleware(print1, print2, print3)

// Pass enhancer as the second arg, since there's no preloadedState
const store = createStore(rootReducer, middlewareEnhancer)

export default store

Как следует из их названий, каждое middleware выводит число при диспетчеризации действия.

Что произойдёт, если мы выполним диспетчеризацию?

src/index.js
import store from './store'

store.dispatch({ type: 'todos/todoAdded', payload: 'Learn about actions' })
// log: '1'
// log: '2'
// log: '3'

В консоли мы увидим вывод:

Логирование print middleware

Как это работает?

Middleware образуют конвейер обработки вокруг метода dispatch хранилища. При вызове store.dispatch(action) мы фактически вызываем первое middleware в цепочке. Оно может выполнить любые операции при получении действия. Обычно middleware проверяет, относится ли действие к определённому типу, подобно редюсеру. Если тип совпадает, middleware выполняет пользовательскую логику. В противном случае действие передаётся следующему middleware в цепочке.

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

В нашем примере действие проходит через:

  1. Middleware print1 (которое мы видим как store.dispatch)

  2. Middleware print2

  3. Middleware print3

  4. Оригинальный store.dispatch

  5. Корневой редюсер внутри store

Поскольку все эти вызовы синхронны, они завершаются в порядке обратном вызову: middleware print1 запускается первым, но завершается последним.

Написание собственного Middleware

Мы также можем писать собственные middleware. Возможно, вам не придется делать это постоянно, но пользовательские middleware — это отличный способ добавить специфическое поведение в Redux-приложение.

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

// Middleware written as ES5 functions

// Outer function:
function exampleMiddleware(storeAPI) {
return function wrapDispatch(next) {
return function handleAction(action) {
// Do anything here: pass the action onwards with next(action),
// or restart the pipeline with storeAPI.dispatch(action)
// Can also use storeAPI.getState() here

return next(action)
}
}
}

Давайте разберем, что делают эти три функции и какие у них аргументы.

  • exampleMiddleware: Внешняя функция и есть сам "middleware". Она будет вызвана функцией applyMiddleware и получит объект storeAPI, содержащий функции хранилища {dispatch, getState}. Это те же самые функции dispatch и getState, которые являются частью хранилища. Если вы вызовете эту функцию dispatch, действие будет отправлено в начало цепочки middleware. Эта функция вызывается только один раз.

  • wrapDispatch: Средняя функция получает в качестве аргумента функцию next. Эта функция представляет собой следующий middleware в цепочке. Если текущий middleware — последний в последовательности, то next будет оригинальной функцией store.dispatch. Вызов next(action) передает действие следующему middleware в цепочке. Эта функция также вызывается только один раз.

  • handleAction: Наконец, внутренняя функция получает текущее action в качестве аргумента и будет вызываться каждый раз при диспетчеризации действия.

Совет

Вы можете дать этим функциям middleware любые имена, но использование таких имен поможет запомнить, что делает каждая из них:

  • Внешняя: someCustomMiddleware (или как вы назовете свой middleware)
  • Средняя: wrapDispatch
  • Внутренняя: handleAction

Поскольку это обычные функции, мы можем записать их, используя стрелочные функции ES2015. Это позволяет писать короче, потому что стрелочные функции не требуют оператора return, но может быть немного сложнее читать, если вы еще не знакомы со стрелочными функциями и неявным возвратом.

Вот тот же пример, написанный с использованием стрелочных функций:

const anotherExampleMiddleware = storeAPI => next => action => {
// Do something in here, when each action is dispatched

return next(action)
}

Мы по-прежнему вкладываем эти три функции друг в друга и возвращаем каждую функцию, но неявный возврат делает запись короче.

Ваш первый пользовательский Middleware

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

Информация

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

Мы можем написать небольшой middleware, который будет записывать эту информацию в консоль:

const loggerMiddleware = storeAPI => next => action => {
console.log('dispatching', action)
let result = next(action)
console.log('next state', storeAPI.getState())
return result
}

Каждый раз, когда действие диспетчеризуется:

  • Выполняется первая часть функции handleAction, и мы выводим 'dispatching'

  • Мы передаем действие в секцию next, которая может быть другим middleware или настоящей store.dispatch

  • В конечном итоге выполняются редюсеры, состояние обновляется, и функция next возвращает результат

  • Теперь мы можем вызвать storeAPI.getState() и посмотреть, какое новое состояние

  • Завершаем, возвращая то значение result, которое пришло из next middleware

Любой middleware может возвращать любое значение, и именно возвращаемое значение первого middleware в цепочке будет возвращено при вызове store.dispatch(). Например:

const alwaysReturnHelloMiddleware = storeAPI => next => action => {
const originalResult = next(action)
// Ignore the original result, return something else
return 'Hello!'
}

const middlewareEnhancer = applyMiddleware(alwaysReturnHelloMiddleware)
const store = createStore(rootReducer, middlewareEnhancer)

const dispatchResult = store.dispatch({ type: 'some/action' })
console.log(dispatchResult)
// log: 'Hello!'

Давайте попробуем еще один пример. Middleware часто ищут определенное действие, а затем выполняют что-то при его диспетчеризации. Также middleware могут содержать асинхронную логику. Мы можем написать middleware, который с задержкой выводит что-то в консоль при получении определенного действия:

const delayedMessageMiddleware = storeAPI => next => action => {
if (action.type === 'todos/todoAdded') {
setTimeout(() => {
console.log('Added a new todo: ', action.payload)
}, 1000)
}

return next(action)
}

Этот middleware будет искать действия типа "todo added". Каждый раз, обнаруживая такое действие, он устанавливает таймер на 1 секунду, а затем выводит полезную нагрузку (payload) действия в консоль.

Случаи применения Middleware

Итак, что можно делать с помощью middleware? Многое!

При получении действия middleware может выполнять любые операции:

  • Логировать информацию в консоль

  • Устанавливать таймауты

  • Выполнять асинхронные API-вызовы

  • Модифицировать действие

  • Приостанавливать или полностью отменять действие

и всё, что только можно придумать.

В частности, middleware предназначены для работы с побочными эффектами. Кроме того, middleware могут расширять dispatch, принимая объекты, не являющиеся действиями. Подробнее об этом мы поговорим в Части 6: Асинхронная логика.

Redux DevTools

Наконец, остался ещё один важный аспект настройки хранилища.

Redux специально разработан для упрощения отслеживания изменений состояния: когда, где, почему и как оно меняется. Для этого в Redux предусмотрена интеграция с Redux DevTools — расширением, которое показывает историю диспетчеризации действий, их содержимое и изменения состояния после каждого действия.

Redux DevTools доступен как расширение для браузеров Chrome и Firefox. Если вы ещё не установили его, сделайте это сейчас.

После установки откройте инструменты разработчика в браузере. Вы увидите новую вкладку "Redux". Пока она пуста — нам нужно настроить интеграцию с хранилищем Redux.

Подключение DevTools к хранилищу

После установки расширения необходимо добавить специальный усилитель (enhancer) для связи с хранилищем.

Документация Redux DevTools содержит инструкции по настройке, но они сложны. Упростить процесс помогает пакет NPM redux-devtools-extension, который экспортирует функцию composeWithDevTools — замену стандартной Redux-функции compose.

Вот как это выглядит:

src/store.js
import { createStore, applyMiddleware } from 'redux'
import { composeWithDevTools } from 'redux-devtools-extension'
import rootReducer from './reducer'
import { print1, print2, print3 } from './exampleAddons/middleware'

const composedEnhancer = composeWithDevTools(
// EXAMPLE: Add whatever middleware you actually want to use here
applyMiddleware(print1, print2, print3)
// other store enhancers if any
)

const store = createStore(rootReducer, composedEnhancer)
export default store

Убедитесь, что index.js всё ещё диспетчеризует действие после импорта хранилища. Теперь откройте вкладку Redux DevTools. Вы увидите примерно следующее:

Redux DevTools Extension: вкладка действий

Слева отображается список диспетчеризованных действий. При клике на действие справа показываются вкладки:

  • Содержимое объекта действия

  • Полное состояние Redux после обработки редюсером

  • Разница между предыдущим и текущим состоянием

  • (При включении) стек вызовов до строки кода, вызвавшей store.dispatch()

Вот как выглядят вкладки "State" и "Diff" после диспетчеризации действия "add todo":

Redux DevTools Extension: вкладка состояния

Redux DevTools Extension: вкладка сравнения

Это мощные инструменты, которые значительно упрощают отладку приложений и анализ внутренних процессов.

Итоги изученного

Как вы уже видели, хранилище (store) — это центральный элемент любого Redux-приложения. Хранилища содержат состояние (state), обрабатывают действия (actions) запуская редьюсеры (reducers), и могут быть кастомизированы для добавления дополнительных возможностей.

Давайте посмотрим, как сейчас выглядит наше примерное приложение:

Напомним, что мы рассмотрели в этом разделе:

Сводка
  • В Redux-приложениях всегда есть единое хранилище
    • Хранилища создаются с помощью API createStore из Redux
    • Каждое хранилище имеет единый корневой редьюсер
  • Хранилища имеют три основных метода
    • getState возвращает текущее состояние
    • dispatch отправляет действие в редьюсер для обновления состояния
    • subscribe принимает callback-слушатель, который вызывается при каждом диспатче действия
  • Усилители хранилищ (store enhancers) позволяют кастомизировать хранилище при создании
    • Усилители оборачивают хранилище и могут переопределять его методы
    • createStore принимает один усилитель в качестве аргумента
    • Несколько усилителей можно объединить с помощью API compose
  • Middleware — основной способ кастомизации хранилища
    • Middleware добавляются через усилитель applyMiddleware
    • Middleware пишутся как три вложенные друг в друга функции
    • Middleware выполняются при каждом диспатче действия
    • Middleware могут содержать сайд-эффекты
  • Redux DevTools позволяют отслеживать изменения в приложении
    • Расширение DevTools можно установить в браузер
    • Хранилищу нужно добавить усилитель DevTools через composeWithDevTools
    • DevTools показывают диспатченные действия и изменения состояния во времени

Что дальше?

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

Однако каждому приложению нужен пользовательский интерфейс для отображения данных и взаимодействия с пользователем. В Части 5: Пользовательский интерфейс и React мы увидим, как Redux-хранилище взаимодействует с UI, и в частности — как Redux работает в связке с React.