Spec-Zone.ru › Redux 3

Редукторы

Действия описывают факт того, что что-то произошло, но не определяют, как изменяется состояние приложения в ответ. Этим занимается редуктор.

Проектирование формы состояния

В Redux всё состояние приложения хранится как один объект. Хорошо подумать над его формой до написания кода. Какова минимальная запись состояния вашего приложения в виде объекта?

Для нашего приложения задач мы хотим хранить две разные вещи:

  • Текущий выбранный фильтр видимости;
  • Фактический список задач.

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

{
  visibilityFilter: 'SHOW_ALL',
  todos: [
    {
      text: 'Consider using Redux',
      completed: true,
    },
    {
      text: 'Keep all state in a single tree',
      completed: false
    }
  ]
}
Примечание по отношениям

В более сложном приложении вам потребуется, чтобы разные сущности ссылались друг на друга. Мы рекомендуем хранить ваше состояние максимально нормализованным, без вложенности. Храните каждую сущность в объекте, используя идентификатор в качестве ключа, и используйте идентификаторы для ссылки на него из других сущностей или списков. Представьте состояние приложения как базу данных. Этот подход подробно описан в документации normalizr. Например, хранение todosById: { id -> todo } и todos: array<id> внутри состояния будет лучшей идеей в реальном приложении, но мы упрощаем пример.

Обработка действий

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

(previousState, action) => newState

Он так называется, потому что это тип функции, который вы бы передали в Array.prototype.reduce(reducer, ?initialValue). Очень важно, чтобы редуктор оставался чистым. Вот чего никогда не нужно делать внутри редуктора:

  • Изменять свои аргументы;
  • Выполнять побочные эффекты, такие как вызовы API и переходы маршрутизации;
  • Вызывать нечистые функции, например, Date.now() или Math.random().

Мы рассмотрим, как выполнять побочные эффекты в подробном руководстве для продвинутых. Пока просто помните, что редуктор должен быть чистым. При одинаковых аргументах он должен вычислить следующее состояние и вернуть его. Без сюрпризов. Без побочных эффектов. Без вызовов API. Без изменений. Только вычисление.

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

Мы начнём с указания начального состояния. Redux вызовет наш редуктор с состоянием undefined в первый раз. Это наш шанс вернуть начальное состояние нашего приложения:

import { VisibilityFilters } from './actions'

const initialState = {
  visibilityFilter: VisibilityFilters.SHOW_ALL,
  todos: []
}

function todoApp(state, action) {
  if (typeof state === 'undefined') {
    return initialState
  }

  // For now, don't handle any actions
  // and just return the state given to us.
  return state
}

Один из приемов — использовать синтаксис ES6 для аргументов по умолчанию, чтобы написать это более компактно:

function todoApp(state = initialState, action) {
  // For now, don't handle any actions
  // and just return the state given to us.
  return state
}

Теперь давайте обработаем SET_VISIBILITY_FILTER. Единственное, что ему нужно сделать, это изменить visibilityFilter в состоянии. Просто:

function todoApp(state = initialState, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return Object.assign({}, state, {
        visibilityFilter: action.filter
      })
    default:
      return state
  }
}

Обратите внимание:

  1. Мы не изменяем state. Мы создаём копию с помощью Object.assign(). Object.assign(state, { visibilityFilter: action.filter }) тоже неправильно: оно изменит первый аргумент. Вы должны указать пустой объект в качестве первого параметра. Также можно включить предложение об использовании оператора расширения для объектов, чтобы написать { ...state, ...newState } вместо этого.

  2. Мы возвращаем предыдущее state в случае default. Важно возвращать предыдущее state для любого неизвестного действия.

Примечание об Object.assign

Object.assign() — часть ES6, но ещё не поддерживается большинством браузеров. Вам нужно будет либо использовать полифил, либо плагин Babel, или вспомогательную функцию из другой библиотеки, например, _.assign().

Примечание о switch и избыточности

Оператор switch — не реальная избыточность. Реальная избыточность Flux — концептуальная: необходимость отправлять обновление, необходимость регистрации хранилища с диспетчером, необходимость, чтобы хранилище было объектом (и сложности, возникающие, когда вам нужно универсальное приложение). Redux решает эти проблемы, используя чистые редукторы вместо эмиттеров событий.

К сожалению, многие всё ещё выбирают фреймворк на основе того, использует ли он операторы switch в документации. Если вам не нравятся операторы switch, вы можете использовать пользовательскую функцию createReducer , которая принимает карту обработчиков, как показано в “сокращение избыточности”.

Обработка дополнительных действий

У нас есть ещё два действия для обработки! Точно так же, как мы сделали с SET_VISIBILITY_FILTER, мы импортируем ADD_TODO и TOGGLE_TODO действия, а затем расширим наш редуктор, чтобы обработать ADD_TODO.

import { VisibilityFilters, ADD_TODO, TOGGLE_TODO } from './actions'

...

function todoApp(state = initialState, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return Object.assign({}, state, {
        visibilityFilter: action.filter
      })
    case ADD_TODO:
      return Object.assign({}, state, {
        todos: [
          ...state.todos,
          {
            text: action.text,
            completed: false
          }
        ]
      })
    default:
      return state
  }
}

Как и раньше, мы никогда не записываем непосредственно в state или его поля, а вместо этого возвращаем новые объекты. Новое todos равно старому todos, к которому добавлен один новый элемент в конце. Новая задача была создана с использованием данных из действия.

Наконец, реализация обработчика TOGGLE_TODO не должна быть неожиданностью:

case TOGGLE_TODO:
  return Object.assign({}, state, {
    todos: state.todos.map((todo, index) => {
      if (index === action.index) {
        return Object.assign({}, todo, {
          completed: !todo.completed
        })
      }
      return todo
    })
  })

Поскольку мы хотим обновить определённый элемент в массиве, не прибегая к изменениям, мы должны создать новый массив с теми же элементами, кроме элемента в указанном индексе. Если вы часто выполняете такие операции, рекомендуется использовать помощник, такой как immutability-helper, updeep или даже библиотеку, такую как Immutable, которая имеет встроенную поддержку глубоких обновлений. Просто помните, что вы не должны изменять элементы внутри state без предварительного клонирования.

Разделение редукторов

Вот наш код до сих пор. Он довольно объёмный:

function todoApp(state = initialState, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return Object.assign({}, state, {
        visibilityFilter: action.filter
      })
    case ADD_TODO:
      return Object.assign({}, state, {
        todos: [
          ...state.todos,
          {
            text: action.text,
            completed: false
          }
        ]
      })
    case TOGGLE_TODO:
      return Object.assign({}, state, {
        todos: state.todos.map((todo, index) => {
          if (index === action.index) {
            return Object.assign({}, todo, {
              completed: !todo.completed
            })
          }
          return todo
        })
      })
    default:
      return state
  }
}

Есть способ сделать его более понятным? Похоже, что todos и visibilityFilter обновляются совершенно независимо. Иногда поля состояния зависят друг от друга, и требуется больше внимания, но в нашем случае мы можем легко разделить обновление todos на отдельную функцию:

function todos(state = [], action) {
  switch (action.type) {
    case ADD_TODO:
      return [
        ...state,
        {
          text: action.text,
          completed: false
        }
      ]
    case TOGGLE_TODO:
      return state.map((todo, index) => {
        if (index === action.index) {
          return Object.assign({}, todo, {
            completed: !todo.completed
          })
        }
        return todo
      })
    default:
      return state
  }
}

function todoApp(state = initialState, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return Object.assign({}, state, {
        visibilityFilter: action.filter
      })
    case ADD_TODO:
    case TOGGLE_TODO:
      return Object.assign({}, state, {
        todos: todos(state.todos, action)
      })
    default:
      return state
  }
}

Обратите внимание, что todos также принимает state — но это массив! Теперь todoApp просто передаёт ему часть состояния для управления, а todos знает, как обновить только эту часть. Это называется составлением редукторов, и это основной метод построения приложений Redux.

Давайте рассмотрим составление редукторов подробнее. Можем ли мы также выделить редуктор, управляющий только visibilityFilter? Можем.

Ниже импортов давайте используем ES6 деструктуризацию объектов для объявления SHOW_ALL:

const { SHOW_ALL } = VisibilityFilters

Затем:

function visibilityFilter(state = SHOW_ALL, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return action.filter
    default:
      return state
  }
}

Теперь мы можем переписать основной редуктор как функцию, которая вызывает редукторы, управляющие частями состояния, и объединяет их в один объект. Он также больше не должен знать полное начальное состояние. Достаточно, чтобы дочерние редукторы возвращали своё начальное состояние при первом вызове с аргументом undefined.

function todos(state = [], action) {
  switch (action.type) {
    case ADD_TODO:
      return [
        ...state,
        {
          text: action.text,
          completed: false
        }
      ]
    case TOGGLE_TODO:
      return state.map((todo, index) => {
        if (index === action.index) {
          return Object.assign({}, todo, {
            completed: !todo.completed
          })
        }
        return todo
      })
    default:
      return state
  }
}

function visibilityFilter(state = SHOW_ALL, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return action.filter
    default:
      return state
  }
}

function todoApp(state = {}, action) {
  return {
    visibilityFilter: visibilityFilter(state.visibilityFilter, action),
    todos: todos(state.todos, action)
  }
}

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

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

Наконец, Redux предоставляет утилиту под названием combineReducers(), которая выполняет ту же избыточную логику, что и todoApp выше. С её помощью мы можем переписать todoApp так:

import { combineReducers } from 'redux'

const todoApp = combineReducers({
  visibilityFilter,
  todos
})

export default todoApp

Обратите внимание, что это эквивалентно:

export default function todoApp(state = {}, action) {
  return {
    visibilityFilter: visibilityFilter(state.visibilityFilter, action),
    todos: todos(state.todos, action)
  }
}

Вы также можете присвоить им разные ключи или вызывать функции по-разному. Эти два способа написания объединённого редуктора эквивалентны:

const reducer = combineReducers({
  a: doSomethingWithA,
  b: processB,
  c: c
})
function reducer(state = {}, action) {
  return {
    a: doSomethingWithA(state.a, action),
    b: processB(state.b, action),
    c: c(state.c, action)
  }
}

Всё, что делает combineReducers(), это генерирует функцию, которая вызывает ваши редукторы с фрагментами состояния, выбранными в соответствии с их ключами, и объединяет их результаты в один объект. Это не магия. И как другие редукторы, combineReducers() не создаёт новый объект, если все предоставленные ему редукторы не изменяют состояние.

Примечание для пользователей, знакомых с ES6

Поскольку combineReducers ожидает объект, мы можем поместить все редукторы верхнего уровня в отдельный файл, export каждую функцию редуктора и использовать import * as reducers для получения их в виде объекта с именами в качестве ключей:

import { combineReducers } from 'redux'
import * as reducers from './reducers'

const todoApp = combineReducers(reducers)

Поскольку синтаксис import * всё ещё новый, мы больше не используем его в документации, чтобы избежать путаницы, но вы можете встретить его в некоторых примерах сообщества.

Исходный код

reducers.js

import { combineReducers } from 'redux'
import {
  ADD_TODO,
  TOGGLE_TODO,
  SET_VISIBILITY_FILTER,
  VisibilityFilters
} from './actions'
const { SHOW_ALL } = VisibilityFilters

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 TOGGLE_TODO:
      return state.map((todo, index) => {
        if (index === action.index) {
          return Object.assign({}, todo, {
            completed: !todo.completed
          })
        }
        return todo
      })
    default:
      return state
  }
}

const todoApp = combineReducers({
  visibilityFilter,
  todos
})

export default todoApp

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

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

© 2015–2017 Dan Abramov
Licensed under the MIT License.
http://redux.js.org/docs/basics/Reducers.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API