redux что такое reducer
Redux на практике: осваиваем действия в приложении
Хватит ждать, пора действовать! Изучаем действия в приложении на Redux, сразу применяя теоретический материал на практике.
Дизайн макета обновлён. При нажатии на любую из кнопок для обновления состояния пользователь чувствует взаимодействие с приложением:
Пример работы в GIF
Что есть действие в Redux?
При посещении банка кассир узнаёт о вашем намерении снять деньги. WITHDRAWAL_MONEY – оно же является действием. Чтобы получить деньги, нужно взаимодействовать с кассиром.
В отличие от setState(), единственный способ обновить состояние в Redux – сообщить об этом reducer:
На самом деле type не может в подробностях сообщить, что вы хотите сделать. Redux не знает, сколько денег нужно снять. Так будет правильнее:
Представим, что номер банковского счета не нужно сообщать, и тогда этой информации становится достаточно.
Вы не можете серьезно модифицировать функцию type. Дополнительная информация задаётся в payload:
Обработка данных в редукторах
Мы говорили, что reducer принимает два значения – state и action. Простой reducer выглядит так:
Чтобы reducer обработал функцию, нужно задать ему инструкцию, используя switch:
В данном примере switch запустит механизм исполнения действия в приложении. Допустим, у вас было 2 кнопки:
При нажатии на первую кнопку приложение переходит в состояние isOpen:
А при нажатии на вторую обновляется поле isClicked:
В Redux не получится использовать setState(): сначала нужно сообщить о действии.
В Redux-приложении всё проходит через редуктор. Поэтому мы ввели поле action.type, чтобы reducer определял нужное действие:
Важно обрабатывать каждый тип по отдельности. Для этого и нужен switch.
Обработка действия в приложении
Для обновления состояния приложений нужно сообщать action, неважно каким способом. При каждом нажатии кнопки нам нужно отправить действие:
Посмотрите, как это будет работать с каждой из кнопок.
React
React-redux
Все действия имеют одно и то же поле type. Как в банке: пополнение счёта производилось бы на разные суммы. Общая функция DEPOSIT_MONEY, разное количество amount.
Дублирование кода
Давайте уменьшим количество повторяющегося кода. Для этого в Redux есть Action Creators. Это функции, которые создают объекты-действия. Например, в нашем случае можно создать функцию setTechnology, принимающую аргумент «текст»:
Объединяем пройденное
Итак, когда вы приходите в банк, кассир сидит за столом и ожидает клиентов, а сейф с деньгами ждёт своего часа в соседней комнате. В Redux каждый участник цепочки (reducer, action, store) тоже находится на своём месте – в отдельной папке. Обычно создают 3 папки:
В каждой из папок создаем файл index.js, что позволит начать работу каждой функции. Теперь будем работать с нашим приложением из статьи.
store/index.js
Это работает так же, как и ранее. Отличие в том, что хранилище создаётся в отдельном index.js файле. Если нам нужен store, будем писать:
App.js
В чем отличия? В четвертой строке хранилище импортируется из собственной «комнаты». Кроме того, здесь есть компонент, который отвечает за работу кнопок.
Следующая особенность в том, что приложение возвращает массив. Это стало доступно в React 16.
Так это работает для компонента App.js.
Реализация ButtonGroup проще:
ButtonGroup не имеет состояния. Он просто принимает массив названий технологий и с помощью map генерирует button для каждого элемента. В данном примере передаётся массив [«React», «Elm», «React-redux»]. У кнопок есть несколько атрибутов:
Сгенерированная кнопка выглядит так:
Прямо сейчас все отображается правильно, но при нажатии на кнопку пока ничего не происходит.
Так происходит потому, что мы не настроили обработчик кликов. Внутри функции render определим действие для onClick:
Хорошо. Теперь определим dispatchBtnAction.
Не забудьте, что основная цель обработчика – послать действие на обработку. Если нажать на кнопку «React», произойдет следующее действие:
А если нажать на «React-Redux»:
Вот так выглядит функция dispatchBtnAction:
Имеет ли смысл код выше?
e.target.dataset.tech получит атрибут данных с кнопки data-tech. Таким образом, константа tech будет хранить текст кнопки. store.dispatch() – способ
отправки действия в приложении на Redux, а setTechnology() – генератор действий, написанный нами ранее. То есть store.dispatch принимает, а setTechnology – создаёт.
Код на изображении ниже должен помочь разобраться в происходящем:
Что происходит после отправки действия?
Для начала несложный вопрос. Что происходит после нажатия кнопки (и отправки действия)? Какой участник появляется в цепочке Redux?
Правильный ответ – кассир. Здесь действия после отправки проходят через редуктор. Чтобы показать это, залоггируем все действия в приложении, проходящие через него:
reducers/index.js
Reducer возвращает начальное состояние. Через console.log() можно посмотреть, что происходит при нажатии на кнопку.
Действия учитываются при нажатии, значит, все проходит через редукторы, как мы и говорили.
Но есть один нюанс. При запуске приложения учитывается лишнее действие:
Это обычная инициализация, не оказывающая негативного влияния на работу программы.
Создание счётчика редуктора
До текущего момента мы писали приложение, которое ничего не делает. Как кассир, который не умеет делать WITHDRAW_MONEY. Что мы хотим от редуктора? При создании хранилища мы передали initialState в createStore.
Когда пользователь нажимает на любую из кнопок, редуктор должен модифицировать состояние:
Цель последнего действия – обновление состояния. Будем использовать switch для обработки разных действий:
Но теперь в фокусе switch будет action.type. Почему? В качестве операций, которые может выполнить кассир – снять наличные, внести на счет, etc. Наш редуктор выполняет действие SET_TECHNOLOGY, но позднее могут быть и другие. Этот единственный case будет отвечать за переключение технологии на любое значение.
Не забывайте, что во всех прочих случаях не стоит выполнять никаких действий. Нужно просто вернуть текущее состояние state.
Кассир (редуктор) понимает, что вы хотите сделать, но не возвращает никакого ответа. Исправим это:
Что только что произошло – объясняем ниже.
Никогда не меняйте состояние внутри редукторов
Первое, что хочется сделать – изменить state и вернуть его (код ниже). Если вы уже знакомы с хорошим стилем в React, то знаете, что это ошибка:
Редуктор и вернул вот это:
Вместо изменения состояния мы возвращаем новый объект. Он имеет все свойства предыдущего, но находится в другом пространстве.
Кроме того, редукторы должны быть чистыми функциями без вызовов API, обновления значений. Теперь воображаемый кассир выдаёт нам деньги. Попробуем нажимать на кнопки. Работает? Нет! По крайней мере, текст не обновляется. В чём дело?
Обновление хранилища
После снятия денег обычно приходит сообщение о совершении операции. В Redux тоже следует настроить получение уведомлений об успешном обновлении состояния.
В каждом хранилище есть метод store.subscribe(). Он вызывается всякий раз, когда изменяется состояние.
Итак, нам нужно обновить элемент на странице. Для этого используем render.
Посмотрим, как это работает в index.js:
Приложение берёт компонент и отображает его в DOM.
Используя принципы ES6, функцию можно упростить.
После каждого успешного обновления будет повторно отображаться с новыми значениями состояния:
Redux + React: основы
Redux является предсказуемым контейнером состояния для JavaScript приложений. Это позволяет вам создавать приложения, которые ведут себя одинаково в различных окружениях (клиент, сервер и нативные приложения), а также просто тестируются.
Redux решает проблему управления состоянием в приложении, предлагая хранить данные в глобальном State, и централизованно изменяя его.
Установка
Reducer
Это функция, которая принимает на вход команды и изменяет state. Если тип action неизвестен, возвращаем state. Пример реализации на JavaScript:
Redux-store
Store содержит всё дерево состояний приложения. Единственный способ изменить состояние внутри него — отправить на него action.
createStore(reducer)
Store — это не класс. Это просто объект с несколькими методами. Чтобы создать его, передайте свою функцию в createStore
getState()
Возвращает текущее дерево состояний вашего приложения. Он равен последнему значению, которое возвращает store’s reducer.
dispatch(action)
store.dispatch(action) — отправляет команду, и это единственный способ вызвать изменение состояния store.
Store’s reducer будет вызываться с текущим getState() результатом и заданным action, синхронно. Его возвращаемое значение будет считаться следующим состоянием. Он будет возвращен с новым getState(), и слушатели изменений будут немедленно уведомлены.
subscribe(listener)
Добавляет слушателя изменений. Вызывается каждый раз, когда store может быть изменён.
Как это работает вместе
Actions Creators
В store может передаваться много данных, поэтому бывает удобно сделать функции создатели действий.
bindActionCreators()
Превращает объект, значения которого являются actions creators, в объект с теми же ключами, но с каждым action creator, заключенным в dispatch-вызов, чтобы их можно было вызывать напрямую.
Единственный вариант использования для bindActionCreators- это когда вы хотите передать actions creators в компонент, который не знает о Redux, и вы не хотите передавать dispatch или хранить Redux в нем.
Структура проекта
Если у много actions creators, разумно вынести их в отдельный файл, или папку. То же касается Reducer’а.
React-Redux
Provider
connect()
connect — это компонент высшего порядка (HOC), который создаёт новые компоненты.
Рассказ о том, как создать хранилище и понять Redux
Redux — это интересный шаблон, и, по своей сути, он очень прост. Но почему его сложно понять? В этом материале мы рассмотрим базовые концепции Redux и разберёмся с внутренними механизмами хранилищ. Поняв эти механизмы, вы сможете освоиться со всем тем, что происходит, что называется, «под капотом» Redux, а именно — с тем, как работают хранилища, редьюсеры и действия. Это поможет вам вывести на новый уровень отладку приложений, поможет писать более качественный код. Вы будете точно знать, какие именно функции выполняет та или иная строка вашей программы. Мы будем идти к пониманию Redux через практический пример, который заключается в создании собственного хранилища с использованием TypeScript.
Этот материал основан на исходном коде хранилища Redux, написанном на чистом TypeScript. Автор предлагает всем желающим взглянуть на этот код и разобраться с ним. Однако, он указывает на то, что этот проект предназначен для учебных целей.
Терминология
Если вы только недавно начали осваивать Redux, или лишь пролистали документацию, вы, наверняка, встретились с некоторыми терминами, которые, полагаю, стоит рассмотреть прежде чем мы приступим к самому главному.
▍Действия
Не пытайтесь воспринимать действия (actions) как JavaScript API. У действий есть определённая цель — и это нужно понять в первую очередь. Действия информируют хранилище о намерении (intent).
Работая с хранилищем, ему дают указания, например, что-то вроде этого: «Эй, хранилище! У меня есть к тебе просьба. Пожалуйста, обнови дерево состояния, добавив в него эти данные».
Сигнатура действия, при использовании TypeScript для её демонстрации, выглядит так:
Payload (полезная нагрузка) — это необязательное свойство, так как иногда мы можем отправлять в хранилище действия, которые не принимают полезной нагрузки, хотя в большинстве случаев это свойство оказывается задействованным. Тут имеется в виду то, что, создавая действие, мы описываем его, например, так:
Это, по сути, шаблон действия. Но об этом после, а пока — продолжим знакомство с терминологией.
▍Редьюсеры
Редьюсер (reducer) — это всего лишь чистая функция, которая принимает состояние (state) приложения (внутреннее дерево состояния, которое хранилище передаёт редьюсеру), и, в качестве второго аргумента, отправленное хранилищу действие. То есть, выглядит всё это так:
Итак, что ещё надо знать о редьюсерах? Редьюсер, как мы знаем, принимает состояние, и для того, чтобы сделать что-нибудь полезное (вроде обновления дерева состояния), нам нужно отреагировать на свойство type действия (мы только что видели это свойство). Делается это обычно с помощью конструкции switch :
Каждая ветвь case внутри оператора switch позволяет реагировать на разные типы действий, которые участвуют в формировании состояния приложения. Например, предположим, что нам надо добавить свойство с каким-то значением в дерево состояния. Для этого мы выполняем некие действия и возвращаем изменённое состояние:
Следуя концепции чистых функций, мы добиваемся того, что одни и те же входные данные всегда приводят к появлению одних и тех же выходных данных. Редьюсеры — чистые функции, которые обрабатывают динамическое состояние на основе действий. Проще говоря, мы настраиваем их, а всё остальное делается в процессе работы. Они инкапсулируют функции, которые содержат логику, необходимую для обновления дерева состояний, основываясь на тех указаниях (действиях), которые мы им передаём.
▍Хранилище
Мне постоянно приходится видеть, как состояние (state) путают с хранилищем (store). Хранилище — это контейнер, а состояние просто размещается в этом контейнере.
Хранилище — это объект с API, которое позволяет взаимодействовать с состоянием, модифицировать его, читать его значения, и так далее.
Полагаю, мы практически готовы к тому, чтобы приступить к созданию нашего собственного хранилища, и всё то, о чём мы только что говорили, пока выглядящее разрозненным, встанет на свои места.
Мне хотелось бы отметить, что, по сути, функции хранилища заключаются в реализации структурированного процесса обновления свойств в объекте. Собственно говоря, это и есть Redux.
API хранилища
Наше учебное хранилище Redux будет обладать всего несколькими общедоступными свойствами и методами. Затем мы будем использовать хранилище так, как показано ниже, передавая ему редьюсеры и исходное состояние для приложения:
▍Метод Store.dispatch()
Метод позволит нам отдавать хранилищу указания, сообщая ему о том, что мы намереваемся изменить дерево состояния. Эта операция выполняется посредством редьюсера, о чём мы уже говорили выше.
▍Метод Store.subscribe()
Метод subscribe позволит организовать передачу в хранилище функций-подписчиков, интересующихся изменениями дерева состояний. Этим функциям, при изменении дерева состояний, передаются соответствующие сведения.
▍Свойство Store.value
Свойство value будет настроено как геттер, оно возвращает внутреннее дерево состояния (в результате мы сможем получить доступ к свойствам).
Контейнер хранилища
Как мы уже знаем, хранилище содержит состояние, а так же позволяет нам отправлять ему действия, которые нужно выполнить над деревом состояния. Оно позволяет и подписываться на обновления. Начнём работу над классом Store :
Мне нравится писать на TypeScript, тут я тоже пользуюсь его механизмами для того, чтобы указать, что объект state будет состоять из строковых ключей, которым могут соответствовать значения любого типа. Это — именно то, что нужно для работы с нашими структурами данных.
Итак, теперь создадим экземпляр хранилища:
В данный момент вполне можно вызвать метод dispatch :
Итак, в методе dispatch надо обновить дерево состояния. Но сначала зададимся вопросом — а как оно выглядит — это дерево состояния?
▍Структура данных для хранения состояния
Для целей этого материала структура данных состояния будет выглядеть так:
Почему? Мы уже знаем, что редьюсеры обновляют дерево состояния. В реальном приложении у нас было бы множество редьюсеров, которые ответственны за обновление некоей части дерева состояния. Эти части нередко называют «слоями» состояния. Каждым таким слоем управляет некий редьюсер.
▍Обновление дерева состояния
Для того, чтобы следовать шаблону иммутабельного обновления, нам нужно присвоить новое представление состояния свойству state в виде совершенно нового объекта. Этот новый объект включает в себя изменения, которые мы хотели сделать в дереве состояния с помощью действия.
В этом примере на время забудем о существовании редьюсеров и просто обновим состояние вручную:
Разработка редьюсеров
Теперь, когда нам известно, что редьюсер обновляет некий слой состояния, опишем этот слой:
▍Создание редьюсера
В данный момент, учитывая то, что мы уже знаем о редьюсерах, можно понять, как расширять код дальше:
Хорошо, пока всё идёт нормально, но редьюсер должен подключаться к хранилищу для того, чтобы его можно было вызвать, передав ему состояние и действие, которое надо над ним выполнить.
Вернёмся к объекту Store :
Нам нужно сделать так, чтобы в хранилище можно было добавлять редьюсеры:
▍Регистрация редьюсера
Для того, чтобы зарегистрировать редьюсер, мы должны помнить о том, что свойство todos находится в дереве состояний, и мы должны привязать к нему функцию-редьюсер. Напомню, мы собираемся работать со слоем состояния, который называется todos :
▍Вызов редьюсеров в хранилище
Теперь мы собираемся обернуть логику редьюсера в функцию, которая тут названа reduce :
Вот что здесь происходит:
▍Обработка initialState с помощью редьюсера
Механизмы работы с подписчиками
Вы часто будете сталкиваться с термином «подписчик» в мире обозреваемых объектов, где каждый раз, когда обозреваемый объект генерирует новое значение, нас уведомляют об этом через подписку. Подписка — это нечто вроде просьбы: «дай мне данные, когда они окажутся доступными или изменятся».
В нашем случае работа с механизмами подписки будет выглядеть так:
▍Подписчики хранилища
Добавим в хранилище ещё несколько свойств, позволяющих настроить механизм подписки:
Тут мы берём переданную через метод subscribe функцию и, после оформления подписки, вызываем её с передачей ей дерева состояния.
▍Отписка от хранилища
Мы можем подписываться на изменения хранилища, но неплохо было бы реализовать и обратный механизм. Отписка может понадобиться, например, для того, чтобы избежать чрезмерного использования памяти, или из-за того, что некие изменения хранилища нас больше не интересуют.
Всё, что тут нужно сделать — это вернуть замыкание, которое, будучи вызванным, удалит функцию из списка подписчиков:
И это всё, что нам нужно.
Красота механизма подписки заключается в том, что у нас может быть множество подписчиков, что означает, что различные части нашего приложения могут быть заинтересованы в различных слоях состояния.
Полный код хранилища
Вот полный код того что мы сделали:
Как видите, всё не так уж и сложно.
Итоги
Я, благодаря примеру, которым поделился с вами, наконец понял Redux. Надеюсь, он поможет в этом и вам.
Уважаемые читатели! Как вы осваиваете новые технологии?
Введение в Redux и как обновляется состояние в приложении Redux
Сегодня я собираюсь поделиться несколькими основными концепциями Redux без использования какой-либо библиотеки представлений (React или Angular). Это своего рода личная заметка для дальнейшего использования, но она может помочь и другим.
Что такое Redux?
Когда приложение становится больше, становится сложнее управлять состоянием и отлаживать проблемы. Это становится проблемой, чтобы отследить, когда и где состояние изменяется и где изменения должны быть отражены. Иногда пользовательский ввод вызывает некоторый вызов API, который обновляет некоторую модель. Эта модель, в свою очередь, обновляет некоторое состояние или, может быть, другую модель и так далее.
В такой ситуации становится сложно отслеживать изменения состояния. Это происходит главным образом потому, что не существует определенного правила для обновления состояния, и состояние может быть изменено из любой точки приложения.
Redux пытается решить эту проблему, предоставив несколько простых правил для обновления состояния, чтобы оно было предсказуемым. Эти правила являются строительными блоками Redux.
Redux Store:
Теперь у нас есть только одно основное состояние, которое включает все состояния приложения, расположенного в одном месте. Любые изменения, внесенные в State Tree, отражаются во всем приложении, поскольку это единственный источник данных для приложения. И это первый фундаментальный принцип Redux.
Состояние всего вашего приложения хранится в дереве объектов в одном хранилище.
Способы взаимодействия с State Tree:
Давайте поговорим о методах, которые store дает нам для взаимодействия с state.
Теперь, когда у нас есть хранилище, которое содержит дерево состояний и несколько способов взаимодействия с состоянием, как мы можем обновить состояние приложения?
Обновление состояния в приложении:
Action могут содержать столько информации, сколько вы хотите. Хорошей практикой является предоставление меньшего количества необходимой информации.
Здесь у нас есть action, чтобы добавить книгу в корзину.
Теперь у нас есть store, state и action в нашем приложении для выполнения некоторых задач. Теперь нам нужен способ использовать эти action для обновления. Это можно сделать с помощью чистой функции, и это правило № 3.
Нам нужна простая чистая функция, которая в качестве параметра принимает текущее state приложения и action, которое нужно выполнить с состоянием, а затем возвращает обновленное состояние. Эти функции называются reducer.
Они называются reducer, потому что они принимают коллекцию значений, переводят ее в обновленное состояние и затем возвращают. Поскольку reducer являются чистыми функциями, они не изменяют исходное состояние. Вместо этого они возвращают обновленное состояние в новом объекте. Наше приложение может иметь один или несколько reducer. Каждый reducer может иметь соответствующее action для выполнения определенных задач.
Поскольку reducer являются чистыми функциями, они должны иметь следующие атрибуты:
Процесс.
Что мы тут узнали?
Давайте подведем итог тому, что мы тут узнали, чтобы подвести итоги.
Поскольку Redux является независимой библиотекой, которую можно использовать с React, Angular или любой другой библиотекой, я избегал создания примера приложения с любой из этих библиотек представлений. Вместо этого я сосредоточился только на основных концепциях Redux.