Потихоньку погружаясь в redux и начиная осозновать правила мироздания его идеологии, многие приходят к следующему вопросу: "А куда я должен прилепить свои запросы к серверу?". В ответ на этот вопрос, вас знакомят с понятием 'middleware' функций, что на русский можно перевести как усилитель. И если в то, зачем эти функции нужны и почему такие вещи как запросы к API нужно именно здесь понять можно. А вот взгляд на синткасис объявления 'middleware' у человека недавно познакомившейся со всей этой кухней react, redux, es6 и прочего веселья вызывает ужас вперемешку с криками 'ЧООО?' и расшатывание только восстановившейся психики после привыкания к предыдущим новым терминам и концепциям. Давайте взглянем на это и попытаемся спуститься глубже, чтобы понять как устроена middleware.
const middleware = store => next => action => {
}

Да, да господа. Middleware - ф-ция, которая принимает store, возвращает ф-цию которая принимает dispatch, которая возвращает ф-цию которая принимает action. Заправлено это всё лямбда синтаксисом из ES6. Зачем? Источники говорят, что это чудо пришло к нам из функционального программирования и лежащим в его основе мат. аппарата. Можно почитать здесь.
Мы получили, что middleware функция по факту принимает 3 аргумента: store, next и action. Теперь предлагаю взглянуть в код redux и посмотреть, как там всё устроено.
Структура библиотеки выглядит так:

Redux представляет нам api из 5 функций: createStore, combineReducers, applyMiddleware, bindActionCreators, compose. Сегодня я хочу остановиться лишь на рассмотрении трех из них (подчеркнуты).
Начнём с createStore, т.к. она является базовой и в примерах создания redux приложения перво-наперво вы сталкиваетесь именно с ней. Ниже вы можете увидеть часть кода, которая будет нас интересовать.
export default function createStore(reducer, preloadedState, enhancer) {
if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
enhancer = preloadedState
preloadedState = undefined
}
if (typeof enhancer !== 'undefined') {
if (typeof enhancer !== 'function') {
throw new Error('Expected the enhancer to be a function.')
}
return enhancer(createStore)(reducer, preloadedState)
}
if (typeof reducer !== 'function') {
throw new Error('Expected the reducer to be a function.')
}
let currentReducer = reducer
let currentState = preloadedState
let currentListeners = []
let nextListeners = currentListeners
let isDispatching = false
function dispatch(action) {
if (!isPlainObject(action)) {
throw new Error(
'Actions must be plain objects. ' +
'Use custom middleware for async actions.'
)
}
if (typeof action.type === 'undefined') {
throw new Error(
'Actions may not have an undefined "type" property. ' +
'Have you misspelled a constant?'
)
}
if (isDispatching) {
throw new Error('Reducers may not dispatch actions.')
}
try {
isDispatching = true
currentState = currentReducer(currentState, action)
} finally {
isDispatching = false
}
const listeners = currentListeners = nextListeners
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener()
}
return action
}
dispatch({ type: ActionTypes.INIT })
return {
dispatch,
subscribe,
getState,
replaceReducer,
[$$observable]: observable
}
createStore как ясно из названия, возвращает хранилище приложения. Аргументами функции могут являться 3 параметра: редьюсер, начальное состояние приложение и enchancer (по факту, эта функция которая возвращается после вызова applyMiddleware). Функция начинается с обработки входных аргументов и выяснения того, имеются ли в наличии какие-либо 'middleware'. Рассмотрены также ситуация, что начальное состояние не было укзаано, а вторым аргументом передана функция, тогда второй аргумент считается что это и есть 'усилитель'. Если усилитель имеется, то вместо хранилища возвращается результат выполнения этой функции. Т.е. поток управления передаётся на неё.
return enhancer(createStore)(reducer, preloadedState)
Иначе формируется объект состояния приложения. В котором нас интересуют лишь свойства dispatch и getState.
return {
dispatch,
subscribe,
getState,
replaceReducer,
[$$observable]: observable
}
Принцип работы getState вы, надеюсь, поймете сами, а вот dispatch рассмотрим вместе. В концепции redux dispatch - это функция, которая принимает событие, передает его редьюсеру, получает от него новый объект и посылает сигнал всем, кто подписался на изменение состояния.
В общих чертах createStore мы рассмотрели. Теперь можно обратиться к коду applyMiddleware
import compose from './compose'
export default function applyMiddleware(...middlewares) {
return (createStore) => (reducer, preloadedState, enhancer) => {
const store = createStore(reducer, preloadedState, enhancer)
let dispatch = store.dispatch
let chain = []
const middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
}
chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch
}
}
}
applyMiddleware принимает массив middleware функций, формат которых мы рассмотрели вначале статьи, а возвращает функцию усилитель (в коде createStore она была параметром enchance). Здесь же мы видим, что applyMiddleware возвращает лямбду в лямбде. Я предлагаю, попытаться абстрагироваться от такого подхода и представлять синтаксис (x) => (y) => {}, как функцию принимающую 2 аргумента x, y.
Соответственно, наша функция возвращает функцию, которая принимает 4 аргумента. Если вернуться назад и посмотреть, место вызоыва этой функции, то можно увидеть, что передаются в ней на самом деле 3 аргумента.
return enhancer(createStore)(reducer, preloadedState)
Enchance - остается пустой. И после вызова функции createStore из applyMiddlware мы получим просто объект хранилища, который будет содержать описанный выше dispatch и другие менее интересные нам вещи.
const store = createStore(reducer, preloadedState, enhancer)
Следующая наша задача - преобразовать исходный dispatch, который формирует новый state и сигнализирует об изменеии в цепочку вызовов, обновления состояние в котором происходит в последнюю очередь.
let dispatch = store.dispatch
let chain = []
const middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
}
chain = middlewares.map(middleware => middleware(middlewareAPI))
Выше можно увидеть, практическое применение разбиения функции от трех аргументов на три функции, каждая из которых принимает по аргументу. Объект middlewareAPI по сути является аргументом store и с помощью функции map, всем middleware функциям проставляется этот аргумент. По факту, сейчас была произведено отложенное проставление одного из аргументов, а именно store во все функции в массиве.
dispatch = compose(...chain)(store.dispatch)
.Следующим этапом является преобразование dispatch в цепочку функций. Функция compose же выглядит так:
export default function compose(...funcs) {
if (funcs.length === 0) {
return arg => arg
}
if (funcs.length === 1) {
return funcs[0]
}
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
Чтобы понять данный код, стоит воспользоваться описанием функции reduce, также чтобы увидеть как dispatch становится списком функций завязанных друг с другом через next, можно повыполняв пошагово код инициализации store и дойдя до этого участка. Каждая функция должна вызывать next(action), чтобы не прервать цепочку. В конце можно увидеть, что результатом работы createStore с указанным 3 аргументом с вызывом в нём applyMiddleware будет объект хранилища, но с преобразованным dispatch
return {
...store,
dispatch
}
На сегодня я вас покину. Удачи!