Введение в Redux

Redux решает проблему управления состоянием в приложении Redux предлагает хранить state в одном “глобальном” объекте.

Функция Reducer обновляет глобальный state в ответ на Actions (действия). Объект Store координирует обновления.


Мотивация

По мере того как требования к одностраничным JavaScript приложениям становятся все более высокими, мы вынуждены управлять все большим количеством состояний (State) с помощью JavaScript. Эти состояния могут включать в себя ответы сервера, кэшированные данные, а также данные, созданные локально, но еще не сохраненные на сервере. Это также относится к UI-состояниям, таким как активный маршрут (route), выделенный таб, показанный спиннер или нумерация страниц и т.д.

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

Это достаточно скверно, принимая во внимание новые требования, становящиеся обычными для фронтэнд-разработки, такие как: обработка оптимистичных обновлений (optimistic updates), рендер на сервере, извлечение данных перед выполнением перехода на страницу и так далее. Как frontend-разработчики, мы пытаемся совладать со сложностью, с которой мы никогда не имели дела прежде, и поэтому неизбежно задаемся вопросом: настало время сдаться?

Эта сложность возникает из-за того, что мы смешиваем две концепции, которые очень нелегки для понимания: изменения (mutation) и асинхронность (asynchronicity). Я называю их Ментос и Кола. Обе эти концепции могут быть прекрасными по отдельности, но вместе они превращаются в бардак. Библиотеки, аналогичные React, пытаются решить эту проблему на уровне представления, удаляя асинхронность и прямое манипулирование DOM. Тем не менее, React оставляет управление состоянием данных за вами. И тут в дело вступает#Redux.

Идя по следам FluxCQRS и Event SourcingRedux пытается сделать изменения состояния предсказуемыми, путем введения некоторых ограничений на то, как и когда могут произойти обновления. Эти ограничения отражены в трех принципах Redux.

Три принципа

#Redux может быть описан тремя фундаментальными принципами:

Единственный источник правды

Состояние всего вашего приложения сохранено в дереве объектов внутри одного стора.

Это облегчает создание универсальных приложений. Состояние на сервере может быть сериализировано и отправлено на клиент без дополнительных усилий. Это упрощает отладку приложения, когда мы имеем дело с единственным деревом состояния. Вы также можете сохранять состояние вашего приложения для ускорения процесса разработки. И с единственным деревом состояния вы получаете функциональность типа Undo/Redo из коробки.

console.log(store.getState())
 
{
  visibilityFilter: 'SHOW_ALL',
  todos: [
    {
      text: 'Consider using Redux',
      completed: true,
    },
    {
      text: 'Keep all state in a single tree',
      completed: false
    }
  ]
}

Состояние только для чтения

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

Это гарантирует, что представления или функции, реагирующие на события сети (network callbacks), никогда не изменят состояние напрямую. Поскольку все изменения централизованы и применяются последовательно в строгом порядке, поэтому нет необходимости следить за “гонкой состояний”. Экшены — это всего лишь простые объекты, поэтому они могут быть залогированы, сериализированы, сохранены и затем воспроизведены для отладки или тестирования.

store.dispatch({
  type: 'COMPLETE_TODO',
  index: 1
})
 
store.dispatch({
  type: 'SET_VISIBILITY_FILTER',
  filter: 'SHOW_COMPLETED'
})

Мутации написаны, как чистые функции

Для определения того, как дерево состояния будет трансформировано экшенами, вы пишете чистые редюсеры.

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

function visibilityFilter(state = 'SHOW_ALL', action) {
  switch (action.type) {
    case 'SET_VISIBILITY_FILTER':
      return action.filter
    default:
      return state
  }
}
 
function todos(state = [], action) {
  switch (action.type) {
    case 'ADD_TODO':
      return [
        ...state,
        {
          text: action.text,
          completed: false
        }
      ]
    case 'COMPLETE_TODO':
      return state.map((todo, index) => {
        if (index === action.index) {
          return Object.assign({}, todo, {
            completed: true
          })
        }
        return todo
      })
    default:
      return state
  }
}
 
import { combineReducers, createStore } from 'redux'
let reducer = combineReducers({ visibilityFilter, todos })
let store = createStore(reducer)

Вот и все! Теперь вы знаете все о Redux.