Редьюсер что это
Перейти к содержимому

Редьюсер что это

  • автор:

Редьюсеры¶

Редьюсеры (Reducers) определяют, как состояние приложения изменяется в ответ на экшены, отправленные в стор. Помните, что экшены только описывают, что произошло, но не описывают, как изменяется состояние приложения.

Проектирование структуры состояния¶

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

Для нашего todo-приложения, мы хотим хранить две разные сущности:

  • Состояние фильтра видимости;
  • Актуальный список todo-задач.

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

 1 2 3 4 5 6 7 8 9 10 11 12 13
 visibilityFilter: 'SHOW_ALL', todos: [  text: 'Consider using Redux', completed: true, >,  text: 'Keep all state in a single tree', completed: false > ] > 

Заметка об отношениях

В более сложном приложении вы, скорее всего, будете иметь разные сущности, которые будут ссылаться друг на друга. Мы советуем поддерживать состояние (state) в настолько упорядоченном виде, насколько это возможно. Старайтесь не допускать никакой вложенности. Держите каждую сущность в объекте, который хранится с ID в качестве ключа. Используйте этот ID в качестве ссылки из других сущностей или списков. Думайте о состоянии приложения (app state), как о базе данных. Этот подход детально описан в документации к normalizr. Например, в реальном приложении хранение хеша todo-сущностей todosById: < id ->todo > и массива их ID todos: array в состоянии (state) было бы лучшей идеей, но мы оставим пример простым.

Обработка экшенов¶

Теперь, когда мы определились с тем, как должны выглядеть наши объекты состояния (state objects), мы готовы написать редьюсер для них. Редьюсер (reducer) — это чистая функция, которая принимает предыдущее состояние и экшен (state и action) и возвращает следующее состояние (новую версию предыдущего).

(previousState, action) => newState; 

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

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

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

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

Мы начнем с определения начального состояния (initial state). В первый раз Redux вызовет редьюсер с неопределенным состоянием( state === undefined ). Это наш шанс инициализировать начальное состояние приложения:

 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
import  VisibilityFilters > from './actions'; const initialState =  visibilityFilter: VisibilityFilters.SHOW_ALL, todos: [], >; function todoApp(state, action)  if (typeof state === 'undefined')  return initialState; > // Пока не обрабатываем никаких экшенов // и просто возвращаем состояние, которое приняли в качестве параметра return state; > 

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

1 2 3 4 5
function todoApp(state = initialState, action)  // Пока не обрабатываем никаких экшенов // и просто возвращаем состояние, которое приняли в качестве параметра return state; > 

Теперь давайте начнем обрабатывать экшен SET_VISIBILITY_FILTER . Все, что нужно сделать — это изменить visibilityFilter в состоянии приложения. Это просто:

 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
import  SET_VISIBILITY_FILTER, VisibilityFilters, > from './actions'; /// . 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 >) тоже неверный вариант: в этом случае первый аргумент будет изменен. Вы должны передать первым аргументом пустой объект. Вы также можете подключить object spread operator proposal, чтобы вместо этого писать < . state, . newState >.
  2. Мы возвращаем предыдущую версию состояния ( state ) в default ветке. Очень важно возвращать предыдущую версию состояния ( state ) для любого неизвестного/необрабатываемого экшена ( action ).

Обратите внимание на Object.assign

Object.assign() это часть ES6, но этот метод не поддерживается старыми браузерами. Вам нужно будет использовать полифилл, плагин для Babel, либо хелпер из другой библиотеки, к примеру _.assign() из lodash.

Обратите внимание на switch и шаблон (boilerplate)

Конструкция switch не является реальным требованием. Реальный шаблон Flux определяется концепцией: необходимость инициировать обновление, необходимость зарегистрировать стор ( Store ) в Dispatcher’е , необходимость, чтобы Store был объектом (возникают осложнения, если вы хотите универсальное приложение (universal app)). Redux решает эти проблемы благодаря использованию чистых редьюсеров вместо генераторов событий (event emitters)

Если вам не нравится конструкция switch , вы можете использовать собственную функцию createReducer , которая принимает объект обработчиков, как показано в “упрощение шаблона (reducing boilerplate)”.

Обрабатываем больше экшенов¶

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

 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
import  ADD_TODO, TOGGLE_TODO, SET_VISIBILITY_FILTER, VisibilityFilters, > 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 , в конец которого добавлен новый элемент todo . Свежий tod был создан с использованием информации, полученной из action .

Ну и наконец, имплементация обработчика для экшена TOGGLE_TODO не должна стать для Вас большим сюрпризом:

 1 2 3 4 5 6 7 8 9 10 11
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 , пока вы его не склонировали.

Разделение редьюсеров¶

Вот так выглядит наш код на данный момент. Выглядит излишне многословным:

 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
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 обновляются совершенно независимо. Иногда поля состояния (state fields) зависят от других полей и требуется большая связанность, но в нашем случаем мы безболезненно можем вынести обновление todos в отдельную функцию:

 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
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: return Object.assign(<>, state,  todos: todos(state.todos, action), >); case TOGGLE_TODO: return Object.assign(<>, state,  todos: todos(state.todos, action), >); default: return state; > > 

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

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

Ниже нашего импорта давайте использовать ES6 Object Destructuring, чтобы объявить SHOW_ALL :

const  SHOW_ALL > = VisibilityFilters; 
1 2 3 4 5 6 7 8
function visibilityFilter(state = SHOW_ALL, action)  switch (action.type)  case SET_VISIBILITY_FILTER: return action.filter; default: return state; > > 

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

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

 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
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 следующим образом:

1 2 3 4 5 6 7 8
import  combineReducers > from 'redux'; const todoApp = combineReducers( visibilityFilter, todos, >); export default todoApp; 

Обратите внимание, что это полностью эквивалентно такому коду:

1 2 3 4 5 6 7 8 9
export default function todoApp(state = <>, action)  return  visibilityFilter: visibilityFilter( state.visibilityFilter, action ), todos: todos(state.todos, action), >; > 

Вы также можете назначать им разные ключи или вызывать функции по-разному. Есть два совершенно равноценных способа писать комбинированные редьюсеры:

1 2 3 4 5
const reducer = combineReducers( a: doSomethingWithA, b: processB, c: c, >); 
1 2 3 4 5 6 7
function reducer(state = <>, action)  return  a: doSomethingWithA(state.a, action), b: processB(state.b, action), c: c(state.c, action), >; > 

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

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

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

1 2 3 4
import  combineReducers > from 'redux'; import * as reducers from './reducers'; const todoApp = combineReducers(reducers); 

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

Исходный код¶

 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
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-стор, которое содержит состояние и заботится о вызове редьюсеров, когда вы отправляете экшен.

React. MVC. Redux

Model-View-Controller (MVC) — это очень часто используемый шаблон проектирования программного обеспечения для реализации пользовательских интерфейсов.

Концепция MVC позволяет разделить данные (модель), представление и обработку действий (производимую контроллером) пользователя на три отдельных компонента:

  • Модель (Model):
    • Предоставляет знания: данные и методы работы с этими данными;
    • Реагирует на запросы, изменяя своё состояние;
    • Не содержит информации, как эти знания можно визуализировать;
    • контролирует ввод данных пользователем и использует модель и представление для реализации необходимой реакции.

    React в роли View

    React.js это библиотека для создания интерфейсов от Facebook. Все аспекты его использования мы рассматривать не будем, речь пойдет про Stateless-компоненты и React исключительно в роли View. Это пример реализации слоя View на React:

    class FormAuthView extends React.Component  componentDidMount()  this.props.tryAutoFill(); > render()  return ( div> input type = "text" value = this.props.login> onChange = this.props.loginUpdate> /> input type = "password" value = this.props.password> onChange = this.props.passwordUpdate> /> button onClick = this.props.submit>> Submit /button> /div> ); > >

    Redux в роли Model

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

    Использование Redux подразумевает существование одного единственного объекта Store, в State которого будет хранится состояние всего вашего приложения, каждого его компонента.

    Чтобы создать Store, в Redux есть функция createStore.

    Синтаксис createStore(reducer, [preloadedState], [enhancer]);

    Её единственный обязательный параметр это Reducer. Reducer это такая функция, которая принимает State и Action, и в соответствии с типом Action определенным образом модифицирует иммутабельный State, возвращая его измененную копию. Это единственное место в нашем приложении, где может меняться State.

    React-redux в роли Controller

    Все React-компоненты так или иначе будут получать свой State и Callback-и для его изменения только через Props. При этом ни один React-компонент не будет знать о существовании Redux и Actions вообще, и ни один Reducer или ActionCreator не будет знать о React-компонентах. Данные и логика их обработки полностью отделены от их представления.

    Никаких “Умных” компонентов не будет.

    Что нам дает такой подход

    • Использование только Stateless-компонентов. Большую часть которых можно написать в виде Functional-component, что является рекомендованным подходом, т.к. они быстрее всего работают и потребляют меньше всего памяти
    • React-компоненты можно переиспользовать с разными контроллерами или без них
    • Легко писать тесты, ведь логика и отображение не связаны между собой
    • Можно реализовать Undo/Redo и использовать Time Travel из Redux-DevTools
    • Не нужно использовать Refs
    • Жесткие правила при разработке делают код React-компонентов однообразным
    • Отсутствуют проблемы с серверным рендерингом

    Что будет, если отступить от MVC?

    Велик соблазн сделать какие-то компоненты поудобнее и написать их код побыстрее, завести внутри компонента State. Мол какие-то его данные временные, и хранить их не нужно. И всё это будет работать до поры до времени, пока, например, вам не придется реализовать логику с переходом на другой URL и возвращением обратно — тут всё сломается, временные данные окажутся не временными, и придется всё переписывать. При использовании Stateful-компонентов, чтобы достать их State, придется использовать Refs. Такой подход нарушает однонаправленность потока данных в приложении и повышает связность компонентов между собой. И то и другое — плохо.

    no-mvc

    Заключение

    Если есть возможность в полной мере использовать концепцию MVC, то давайте её использовать, и не нужно изобретать что-то другое. Это подход проверенный на прочность десятилетиями, и все его плюсы, минусы и подводные камни давно известны. Придумать что-то лучше навряд ли получится.

    Redux

    Зачем?

    Вы могли заметить, что React не предоставляет (но и не навязывает) способов работы со слоем данных и/или бэкэндом на фронте. Поток данных в JSX через props не подходит для этого, так как одна и та же информация с бэка (например о текущем залогиненном пользователе) может применяться в отображении во множестве компонентов разных размеров и глубины вложенности. Попытки передать такую информацию через props из главного компонента App привели бы к сильному загрязнению кода JSX всякими props с данными и множественной копипасте.

    Компоненты и хранилище.

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

    Связи при наличии хранилища предполагают разделение ответственности:

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

    Как?

    • Само хранилище реализуется объектом, запрещенным к изменению напрямую;
    • Запрос на изменение хранилища присылается в форме объекта (action), который обрабатывается специальной функцией-редьюсером.
    • Редьюсер создает новый объект хранилища и возвращает его.
    • Хранилище обновляется и оповещает подписчиков.

    Итого: минимальная реализация redux занимает строк 50.

    Для связывания компонентов с redux используется подход HOC — high-order components, аналог функций высшего порядка, только для компонентов. Суть в том, что на базе вашего компонента создается компонент-обертка, которая передает в ваш компонент нужные части хранилища и функции для изменения хранилища используя props. Таким образом компонент автоматически отображает изменения в хранилище и имеет возможность послать запрос на изменение в хранилище.

    Redux Basic

    import Provider, connect> from 'react-redux'; import createStore, combineReducers> from 'redux'; let store = createStore((state, action) =>  //единственный редьюсер данного хранилища if (state === undefined) //redux запускает редьюсер хотя бы раз, что бы инициализировать хранилище return counter: 0>; //обязательно вернуть новый объект, а не изменить текущий state > if (action.type === 'COUNTER_INC') //в каждом action должен быть type return counter: state.counter +1> //создаем новый объект базируясь на данных из предыдущего состояния > if (action.type === 'COUNTER_DEC') return counter: state.counter -1> > return state; //редьюсеров может быть несколько, в таком случае вызываются все редьюсеры, но далеко не всегда action.type будет относится к этому редьюсеру. Тогда редьюсер должен вернуть state как есть. >) store.dispatch( type: 'COUNTER_INC' >) store.dispatch( type: 'COUNTER_DEC' >)
    • createStore создаёт новое хранилище. В качестве параметра передается одна функция-редьюсер, которая обрабатывает все запросы на изменение хранилища
    • Редьюсер принимает текущее состояние хранилища и объект action — действие над хранилищем.
    • Когда запускается createStore редьюсер запускается с state = undefined для первоначальной инициализации хранилища
    • В action обязательно должно быть поле type
    • Редьюсер обязан возвращать каждый раз новый объект, а не модифицировать старый.
    • Если редьюсер не знает переданный тип действия, он должен вернуть state как есть.
    • метод dispatch посылает объект-действие в хранилище для обработки редьюсером

    Redux usual

    В обычной ситуации один редьюсер с большим количеством действий смотрится не очень. Посему в комплекте с redux идет функция combineReducers, которая:

    • Воспринимает не один редьюсер (как createStore), а объект, ключами в котором являются ветви хранилища, а значениями — редьюсеры, работающие с этой ветвью.
    • Редьюсеры получают не весь state, а только свою ветвь.
    • Редьюсер возвращает не весь state, а только свою ветвь.
    • Однако редьюсеры получают все действия: и свои, и чужие. Посему не забываем возвращать state неизменным если действие не относится к этому редьюсеру.
    const reducers = combineReducers( c: counterReducer, b: booleanReducer >) //создаем функцию-обертку, которая запустит последовательно counterReducer и booleanReducer передав им ветви c и b хранилища // и обновив эти же ветви в случае нового состояния.

    actionCreators

    По феншую вы не отправляете объект действия непосредственно литеральным параметром dispatch, а создаете функцию, которая на базе нужной информации возвращает объект действия. Это удобно при работе с компонентами React.

    function actionInc() return  type: 'COUNTER_INC' > > function actionDec() return  type: 'COUNTER_DEC' > > store.dispatch(actionInc()) store.dispatch(actionDec())

    React-Redux

    Компоненты связываются с redux гибким способом:

    • Все компоненты, которые вы хотите присоединить к тому или иному хранилищу должны находится внутри />. Таким образом вы можете переподключить все вложенные компоненты к другому хранилищу в одном месте. Хотя обычно предполагается что хранилище одно на приложение.
    • При создании компонента-обертки, связанного с redux вы можете указать какие части хранилища подключить к каким props; а так же передать через props набор функций, создающих действия (actionCreator), для отправки данных из компонента в хранилище.
    class Counter extends Component render() return ( div> button onClick=this.props.actionInc>>+/button> span>this.props.counter>/span> button onClick=this.props.actionDec>>-/button> /div> ); > > let mapStateToProps = state => (counter: state.c.counter>) //функция должна из state достать нужное значение, которое // попадет в props let mapDispatchToProps = actionInc, actionDec>; //actionCreator-ы, тут переданные, автоматом оборачиваются в dispatch let ConnectedCounter = connect(mapStateToProps,mapDispatchToProps)(Counter) //connect возвращает функцию, которая из любого // компонента сделает компонент с props из mapStateToProps и mapDispatchToProps let ConnectedViewCounter = connect(mapStateToProps)(props => div>props.counter>/div>)

    Компонент Counter из примера выше готов принять через props 3 параметра — функции для увеличения и уменьшения счетчика, и само значение счетчика. Используя connect можно получить компонент-обертку (тут этот класс хранится в переменной ConnectedCounter), который будет использовать ваш компонент, но передавать ему нужные данные из store и обеспечивать возможностью отправить данные в store.

    connect

    • Первый параметр — функция, которая собирает нужную информацию из хранилища (в функцию передается текущее состояние хранилища) и возвращает объект с ключами, которые вам нужны в props.
    • Второй параметр — объект, в котором ключами будут опять же свойства в props, а значениями — actionCreator-ы. connect автоматически обернет ваши actionCreator в dispatch, после чего можно спокойно вызывать this.props.actionName() — это вызовет store.dispatch(actionName())

    connect возвращает функцию, которая из любого компонента сделает компонент-обертку с вышеуказанными настройками.

    На самом деле connect более всеяден в плане параметров, но вы всегда можете это загуглить.

    Written by Vadim Goloviychuk

    Оглавление

    image

    image

    Вот вы прочитали мою статью про React (если нет, то настоятельно рекомендую вам сделать это) и начали разрабатывать приложения на нём. Но что это? Вы замечаете, как с расширением вашего приложения становится всё сложнее следить за текущим состоянием, сложно следить за тем, когда и какие компоненты рендарятся, когда они не рендарятся и почему они не рендарятся, сложно следить за потоком изменяющихся данных. Для этого и есть библиотека Redux. Сам React хоть и лёгкий, но для комфортной разработки на нем нужно много чего изучить.

    И сегодня мы разберём 2 библиотеки: Redux и React-redux. Для использования Redux’а вам не нужно скачивать дополнительных библиотек, но, если использовать его в связке с библиотекой React-redux разработка становится ещё удобнее и проще.

    Все примеры из этой статьи вы можете найти в этом репозитории на Github. Там находится полностью настроенное приложение React с использованием Redux и React-redux. Вы можете использовать его как начальную точку для вашего проекта. Изменяйте названия файлов и добавляйте новые в этот репозитории для создания собственного приложения. Смотрите во вкладку релизы для того что бы найти разные версии приложения. Первая содержит приложение только с использованием Redux, второе с использованием Redux и React-redux.

    Мотивация использования Redux

    Механизм локального хранилища компонента, который поставляется вместе с базовой библиотекой (React) неудобен тем, что такое хранилище изолировано. К примеру, если вы хотите, чтобы разные независимые компоненты реагировали на какое-либо событие, вам придётся либо передавать локальное состояние в виде пропсов дочерним компонентам, либо поднимать его вверх до ближайшего родительского компонента. В обоих случаях делать это не удобно. Код становится более грязным, трудночитаемым, а компоненты зависимыми от их вложенности. Redux снимает эту проблему так как всё состояние доступно всем компонентом без особых трудностей.

    Redux является универсальным средством разработки и может быть использован в связке с различными библиотеками и фреймворками. В этой же статье будет рассматривается использование Redux в React приложениях.

    1. Установка Redux и начало работы

    Используете ли вы Yarn или Npm, выполните одну из этих команд для установки Redux:

    # NPM npm install redux # Yarn yarn add redux 

    Скорее всего вы используете папку src в которой хранится ваша кодовая база. Файлы, связанные с redux принято хранить в отдельной папке. Для этого я использую папку /src/store в которой хранится всё то, что связано с Redux и хранилищем приложения. Вы можете назвать ее по другому или поместить в другое место.

    Создайте базовую структуру для хранилища. Она должна выглядит примерно следующим образом:

    .store
    ├── actionCreators
    │ ├── action_1.js
    │ └── action_2.js
    ├── actions
    │ ├── action_1.js
    │ └── action_2.js
    ├── reducers
    │ ├── reducer_1.js
    │ ├── reducer_2.js
    │ └── rootReducer.js
    ├── initialState.js
    └── store.js

    Конечно здесь я использовал примитивные названия для файлов, это сделано для наглядности. В настоящем проекте так называть файлы не стоит.

    2. Redux

    2.1 createStore

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

    Глобальное хранилище приложения создаётся в отдельном файле, который как правило называется store.js:

    // Код файла store.js import < createStore >from 'redux'; const store = createStore(reducer); export default store;

    2.2 reducer()

    reducer — чистая функция которая будет отвечать за обновление состояния. Здесь реализовывается логика в соответствие с которой будет происходить обновление полей store.

    Так выглядит базовая функция reducer:

    function reducer(state, action) < switch(action.type) < case ACTION_1: return < value: action.value_1 >; case ACTION_2: return < value: action.value_2 >; default: return state; > >

    Функция принимает значение текущего состояния и обьект события (action). Обьект события содержит два свойства — это тип события (action.type) и значение события (action.value).

    К примеру если нужно обработать событие onChange для поля ввода то объект события может выглядеть так:

    Некоторые события могут не нуждаться в передаче каких-либо значении. К примеру, обрабатывая событие onClick мы можем сигнализировать о том, что событие произошло, более никаких данных не требуется, а как на него реагировать будет описывать логика, заложенная непосредственно в сам компонент которой должен на него реагировать и частично в reducer. Но во всех случаях необходимо определять тип события. Редьюсер как бы спрашивает: что произошло? actio.type равен «ACTION_1» ага значит произошло событие номер 1. Дальше его нужно как то обработать и обновить состояние. То, что вернёт редьюсер и будет новым состоянием.

    ACTION_1 и ACTION_2 это константы событий. По-другому Actions. Про них мы поговорим далее 2.5 Actions.

    Как вы уже догадались store может хранить сложную структуру данных состоящих из набора независимых свойств. Обновление одного свойства оставит нетронутым другие свойства. Так из примера выше, когда происходит событие номер один (ACTION_1) обновляется поле номер один (value_1) в store при этом поле номер два (value_2) остаётся нетронутым. В общем механизм схож с методом this.setState().

    2.3 dispatch()

    Что бы обновить store необходимо вызвать метод dispatch(). Он вызывается у объекта store который вы создаёте в store.js. Этот объект принято называть store поэтому обновление состояния в моём случае выглядит так:

    store.dispatch(< type: ACTION_1, value_1: "Some text" >);

    ACTION_1 это константа события о которой речь пойдет дальше (см. Actions).

    Эта функция вызовет функцию reducer который обработает событие и обновит соответствующие поля хранилища.

    2.4 actionCreator()

    На самом деле передавать объект события напрямую в dispatch() является признаком плохого тона. Для этого нужно использовать функцию под названием actionCreator. Она делает ровно то что и ожидается. Создаёт событие! Вызов этой функции нужно передавать как аргумент в dispatch а в actionCreator передавать необходимое значение (value). Базовый actionCreator выглядит следующим образом:

    function action_1(value) < return < type: ACTION_1, value_1: value >; > export default action_1;

    Таким образом вызов dispatch должен выглядеть так:

    store.dispatch(action_1("Some value"));

    С использованием actionCreator код становится более чистым.

    2.5 Actions

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

    const ACTION_1 = "ACTION_1"; export default ACTION_1;

    Опять же в проекте вам стоит называть константы в соответствии с событием, которое она описывает: onClick, createUserSesion, deleteItem, addItem и т.д. Главное, чтобы было понятно. Замете что я нигде не писал import поэтому не забудьте импортировать ваши константы перед их использованием. Потому что константы тоже принято разбивать на отдельные файлы храня их в специальной папке. Хотя некоторые хранят их в одном файле под названием actionTypes.js. Такое решение нельзя назвать не правильным, но и не идеальным.

    2.6 getState()

    С помощью dispatch() обновили, а как теперь посмотреть новое значение store? Ничего изобретать не нужно, есть метод getState(). Он также, как и метод dispatch вызывается на экземпляре объекта store. Поэтому для моего примера вызов

    store.getState()

    вернёт значение полей хранилища. К примеру что бы посмотреть значение поля value_1 необходимо будет вызвать

    store.getState().value_1

    2.7 subscribe()

    А как же узнать, когда состояние обновилось? Для этого есть метод subscribe(). Он также вызывается на экземпляре store. Данный метод принимает функцию, которая будет вызывается каждый раз после обновления store. Он как бы «подписывает» функцию, переданную ему на обновление. К примеру следующий код при каждом обновлении (при каждом вызове dispatch()) будет выводить новое значение store в консоль.

    store.subscribe(() => console.info(store.getState()))

    Этот метод возвращает функцию unsubscribe(). Которая позволяет «отписаться от обновления». К примеру если компонент удаляется из DOM стоит отписать его методы от обновления в componentWillUnmount(). Этот метод жизненного цикла вызывается при размонтировании компонента и это именно то место где стоит отписываться от обновления. Проще говоря в деструкторе.

    2.8 combineReducers()

    combineReducers() позволяет объединить несколько редьюсеров в один.

    Если логика обновления компонентов довольно сложна и\или необходимо обрабатывать большое количество различных типов событий, то корневой reducer может стать слишком громоздким. Лучшим решением будет разбить его на несколько отдельных редьюсеров каждый из которых отвечает за обработку только одного типа событий и обновления определённого поля.

    Когда вы разбиваете базовый редьюсер на несколько, то название каждого из них должно соответствовать полю которое он обновляет в store.

    К примеру если редьюсер обновляет поле номер один, то он может выглядеть так:

    function value_1(state, action) < switch(action.type) < case ACTION_1: return action.value_1; default: return state; >> export default value_1;

    Название редьюсера (value_1) показывает какое свойство он будет обновлять в store. Если переименуете его в value_2 то он станет обновлять value_2. Поэтому учтите это!

    Когда используется единый редьюсер мы показываем какое поле хотим обновить:

     case ACTION_1: return < value_1: action.value_1 >;

    Но когда вы разделили ваши редьюсеры вам нужно просто вернуть новое значение:

    case ACTION_1: return action.value_1;

    Поскольку здесь не требуется указывать которое из полей обновляет редьюсер ибо его название и есть поле которое он обновляет.

    2.9 initialState

    initialState — объект, представляющий начальное состояние хранилища. Он является вторым не обязательным аргументом метода createStore(). С созданием хранилища можно сразу объявить начальное состояние для его полей. Этот объект желательно создавать, даже в тех случаях, когда объявления начального состояния не требуется. Потому что этот объект помогает посмотреть на структуру хранилища и название его полей. Обычный объект initialState выглядит следующим образом:

    const initialState = < date_1: "value. ", date_2: "value. " >; export default initialState;

    В некоторых случаях (когда компонент сразу использует значение из store), его объявление может стать обязательным иначе вы получите ошибку: TypeError: Cannot read property ‘value_1’ of undefined.

    Также редьюсеры всегда должны возвращать по дефолту текущее состояние. К примеру, если используется единый reducer то последнее значение в switch должно выглядеть так:

    default: return store;

    Если же вы разделяете редьюсеры на независимые функции, то он должен возвращать значение того свойства за которое он отвечает:

    default: return store.value_1;

    Также если вы не передаёте объект initialState в createStore вы можете вернуть его из редьюсера. В обоих случаях будет инициализировано начальное состояние для store.

    3. React-redux

    Казалось бы, у нас есть всё что бы использовать Redux. Но на деле использование его без пакета React-redux в React приложениях выглядит не очень красиво.

    3.1 Provider

    Для использование store в компоненте вам необходимо передавать его в пропсы:

    ReactDOM.render( />, document.getElementById('root'));

    И после использовать в компоненте: this.props.state. Для этого react-redux предостовляет метод Provider:

    ReactDOM.render( > , document.getElementById('root'));

    Таким образом метод connect сможет использовать store. В противном случае вы получите ошибку: Error: Could not find «store» in the context of «Connect(Main)». Either wrap the root component in a , or pass a custom React context provider to and the corresponding React context consumer to Connect(Main) in connect options.

    Также можно передать store напрямую в компонент, не оборачивая его в Provider и это будет работать. Но лучше всё-таки используйте Provider.

    3.2 mapStateToProps()

    Этот метод вызывается всякий раз, когда происходит обновление store и именно он передаёт необходимые свойства из store в компонент. К примеру компонент, должен реагировать и обновлять UI каждый раз, когда поле номер один (value_1) обновилось. На обновление других полей ему реагировать не нужно. Если вы не используете React-redux вам бы пришлось использовать метод subscribe() что бы узнавать об обновлении и далее каким то образом проверять обновилось ли поле номер один или нет. В общем несложно понять, что такой код будет выглядеть слишком грязным и избыточным. С помощью mapStateToProps() можно чётко определить какие поля интересуют компонент. И на какие поля он должен реагировать.

    Возвращаясь к примеру выше, если компоненту один нужно получать поле номер один (value_1) то mapStateToProps для него будет выглядеть следующим образом:

    function (state) < return < value_1: state.value_1 >; >

    После внутри компонента мы можем обращается к полю value_1 через this.props.value_1. И каждый раз когда это поле будет обновляется компонент будет рендерится заново.

    Вы можете создать отдельную папку в /src/store для хранения файлов каждый из которых будет содержать функцию mapStateToProps для всех ваших компонентов. Либо (как сделал это я) использовать единую функцию возвращающую функцию mapStateToProps для каждого компонента. Лично мне нравится такой подход. Такая функция выглядит следующим образом:

    function mapStateToProps(component) < switch(component) < case "Component_1": < return function (state) < return < value_1: state.value_1 >; > > case "Component_2": < return function(state) < return < value_2: state.value_2 >; > > default: return undefined; > > export default mapStateToProps;

    Эта функция в качестве аргумента принимает строку с названием компонента и возвращает функцию mapStateToProps которая возвращает объект со свойством из store необходимом для данного компонента. Эту функцию можно назвать mapStateToPropsGenerator().

    3.3 mapDispatchToProps()

    Эта функция передаёт в компонент методы для обновления необходимого поля store. Что бы не вызывать dispatch напрямую из компонента вы будете использовать данный метод для того что бы передавать в props метод вызов которого приведёт к вызову dispatch и обновлению соответствующего поля. Просто теперь это будет выглядеть более элегантно, а код более понятным и чистым.

    К примеру компонент, номер один должен иметь возможность обновлять поле номер один из store. Тогда mapDispatchToProps для него будет выглядеть следующим образом:

    function (dispatch) < return < changeValue_1: bindActionCreators(action_1, dispatch) >; >;

    Теперь для обновления свойства value_1 вы будете вызывать changeValue_1() через this.props.changeValue_1(value). Не вызывая dispatch напрямую через this.props.store.dispatch(action_1(value)).

    bindActionCreators следует импортировать из redux. Он позволяет оборачивать функцию dispatch и actionCreator в единый объект. Вы можете не использовать bindActionCreators но тогда код будет выглядеть избыточным. Вы должны старятся реализовать какую-либо функциональность так, чтобы код выгладил просто и миниатюрно. Поэтому ничего лишнего писать не следует.

    Только чистый и понятный код. Метод bindActionCreators(actionCreator, dispatch) принимает два обязательных параметра это функцию actionCreator о которой мы уже говорили и dispatch. Возвращая метод для изменения полей store.

    Также как и для mapStateToProps я использую функцию генератор возвращающую функцию mapDispatchToProps для каждого компонента:

    import < bindActionCreators >from 'redux'; import action_1 from './actionCreators/action_1'; import action_2 from './actionCreators/action_2'; function mapDispatchToProps(component) < switch(component) < case "Component_1": return function(dispatch) < return < change_value_1: bindActionCreators(action_1, dispatch) >; >; case "Component_2": return function(dispatch) < return < change_value_2: bindActionCreators(action_2, dispatch) >; >; default: return undefined; > > export default mapDispatchToProps;

    3.4 connect()

    Ну и теперь кульминация! То без чего всё это не будет работать. Это функция connect.
    Именно она связывает mapStateToProps и mapDispatchToProps с компонентом и передает необходимые поля и методы в него. Возвращает она новый компонент-обёртку для вашего компонента. Как правильно именовать такой компонент я не знаю, ибо в самой документации React-redux это не описывается. Лично я добавляю окончание _w для компонентов оберток. Как бы _w = wrap Component. Подключение компонента в этм случае выглядит так:

    const COMPONENT_1_W = connect(mapStateToProps("Component_1"), mapDispatchToProps("Component_1"))(Component_1);

    И теперь в ReactDOM.render() вы передаёте не ваш компонент, а тот что возвращает функция connect.

    Если же у компонента нет необходимости в передаче ему mapStateToProps или mapDispatchToProps передавайте undefined или null в него.

    Что такое Редьюсеры: Как использовать их без Redux

    Обработать состояние можно с помощью редьюсера в Class Components, используя функцию, преобразующую действия в изменения состояния. Благодаря этому централизируются все функции setStates.

    �� Что такое Редьюсер?

    Редьюсеры — это функции, которые принимают входные данные и решают, какие действия с ними выполнить в одной центральной точке. ��

    Функция, которая определяет представление для отображения на основе URL-адреса, это и есть редьюсер.

    Redux Reducers™️ используются для анализа событий в приложении и того, как эти события влияют на состояние приложения.

    Вышеприведенный пример запускается с помощью вызова функции dispatch с action (объект, описывающий событие). ��

    Редьюсеры можно использовать в class component, создав функцию, обрабатывающую установку состояния по типу действия:

    Нет необходимости использовать редьюсер в этом простом примере. По этой причине React предоставляет такие хуки, как useState и useReducer .

    Однако, когда я передаю способы изменения состояния, а count сочетается с еще несколькими свойствами состояния, я предпочитаю использовать редьюсер.

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

    С помощью Redux можно подключить ( connect ) глобальное хранилище к компоненту. Можно перевести state в props. Также обеспечивается функция dispatch , запускающая редьюсеры.

    Вместо передачи функции dispatch , выполним передачу функции update , которая работает как setState .

    �� Создание худшей версии Redux

    Функция update указывает, каким образом состояние изменит функцию. Она может находиться рядом с другими аналогичными изменениями состояния.

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

    Имитировать этот шаблон можно даже без изменения redux. Отправленные действия, такие как SET_COUNT , нужно установить в функцию setState . Сделать это очень просто.

    Однако при создании менее самоуверенного действия, такого как INCREMENT_BUTTON_CLICKED , его можно использовать во многих редьюсерах, а полезная нагрузка действия не будет слишком сильно варьироваться.

    �� Редьюсеры можно использовать не только по отношению к состоянию

    Редьюсеры являются отличным способом для согласования решений. Для тех, кто работал с react-router-4, вышеприведенный код покажется знакомым.

    Благодаря компоненту , редьюсеры route-view можно разместить где угодно.

    Теперь на вопрос: “Какими способами URL может изменить то, что отображает” есть ответ.

    �� Подводим итоги

    1. В качестве шаблона, редьюсеры существуют вне Redux и Javascript, к тому же, они просты в реализации. Они выполняют одну определенную функцию — принимают входные данные и выдают результат.
    2. Редьюсеры Redux превращают события приложения в состояние. Чтобы выполнить это, нет необходимости использовать Redux, это можно осуществить с помощью локальных компонентов состояния.
    3. С помощью редьюсеров можно с легкостью организовать и найти различные вариации того, что может произойти в коде. Также они являются ценным инструментом в разработке быстрорастущих приложений.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *