react что такое redux
Основы Redux для начинающих
Что такое Redux?
Redux — это инструмент для управления состоянием приложения. Построен на принципах технологии Flux и функционального программирования. Создан компанией FaceBook, но вопреки распространенному мнению может использоваться не только в связке с React, но также и с другими фреймворками/библиотеками.
Flux — это тип архитектуры и набор паттернов проектирования веб-приложений. Подробнее на Wiki
Redux использует методологию flux. Она состоит из 4 понятий:
В React по умолчанию нет какого-то глобального state (состояния), которое было бы доступно во всем приложении. Вы можете только сохранять данные в рамках одного компонента. К примеру, у вас есть интернет магазин и в нем есть корзина с товарами. Если работать только со стейтом компонента Корзина, то вам эти данные будут недоступны в других компонентах. Также например, у вас есть иконка корзины в углу экрана, которая должна показывать количество товара, которые пользователь добавил туда. Так вот средствами чисто React, это будет сложно реализовать.
Вот именно поэтому есть такие библиотеки как Redux, для хранения всех данных приложения в одном месте и удобного их обновления.
Основные понятия Redux
Как я уже писал выше, основные понятия редакса — actions, dispatcher, store.
Store — это состояние веб-компонента, которое хранит в себе всю информацию (или ту которую вы решили сохранить в него). В дальнейшем стор будет доступен из любого компонента вашего приложения.
Action — действие, описывает что нужно сделать. Согласно принципам функционального программирования, мы не можем изменять объект напрямую, поэтому нам нужны экшены, чтобы передать их в диспатчер и «сказать», что нужно сделать.
Dispatcher — сообщает хранилищу о каком-то действии (action) и передает ему обновленную информацию.
Теперь когда мы разобрали основные понятия, давайте посмотрим как работает Redux:
Схема работы Redux
Компонент генерирует действие (action), диспатчер сообщает об этом хранилищу (store), хранилище изменяет состояние и данные передаются в компонент (View).
Есть еще одно понятие в Redux это reducer (редюсер). Редюсер — это чистая функция, которая принимает как аргумент хранилище и экшен. Основные правила редюсеров:
Более подробно про чистые функции можно прочитать тут.
Простой пример использования Redux
Теперь на простом практическом примере разберем как работать с Redux.
Мы сделаем простое приложение ToDo, которое даст возможность создавать свои таски с сохранением их в store. Это будет простое приложение для примера, основной упор сделан на работу с Redux.
Итак, есть 2 варианта, вы можете скачать стартовый проект и просто запустить установку, или пошагово пройти и создать проект со старта.
Установка и настройка проекта
Чтобы создать приложение заново, открываем командную строку или Git Bash
Далее заходим в папку проекта и устанавливаем Redux и пакет для Реакта — react-redux
Если вы скачали архив с уже готовым приложением, тогда его нужно распаковать, войти в папку с приложением и в командной строке/терминале запустить команду:
Теперь, чтобы запустить наш проект нужно воспользоваться следующей командой:
Проект будет собран и запущен, автоматически откроется вкладка в браузере
Результат выполнения команды npm run start
Для того, чтобы не верстать всё заново, мы используем Bootsrap. Давайте его установим.
Также подключим стили в файле src/index.js
Создание базовой структуры для хранилища
Теперь давайте сделаем базовую структуру для Redux. Создадим папку src/store, а в ней 4 файла
Сначала определим какие типы экшенов нам нужны в файле actionTypes.js. Можно типы и не определять, но в дальнейшем это даст нам возможность сократить время на дебагинг, если вдруг понадобится изменить имя экшена, они все находятся в одном месте, что тоже удобно. Если нам понадобится экшен в другом месте, нам достаточно будет импортировать его в другом модуле.
В нашем приложении, например, нам нужен будет экшен в 2 файлах: actions и reducer. Создадим файл actionTypes.js и в нем определим наши типы:
В файле store/actions.js мы опишем все экшены, которые нам потребуются для приложения:
Выше вы видите типичную структуру экшена: это функция, которая возвращает объект с двумя свойствами:
Теперь давайте рассмотрим функцию редюсер (store/reducer.js):
Тут мы импортируем наши типы экшенов, затем определяем переменную для того, чтобы задавать ID каждому новому таску.
Сам reducer принимает в качестве аргументов state (или равняется пустому массиву) и экшен. Далее мы проверяем тип екшена и в зависимости от этого производим определенные манипуляции со стейтом.
Давайте размеберем на примере экшена TASK_ADD. При добавлении нового таска, нам необходимо сделать копию текущего стейта и добавить к нему новый таск
Далее на основании этого редюсера нам нужно создать store с помощью функции createStore. Создадим файл store.js с таким содержимым:
* У меня также вторым параметром добавлена след. строка
Это для работы плагина для Chrome — Redux DevTools. Удобный плагин для дебагинга.
Теперь после создания store, мы можем использовать его в любом модуле нашего приложения.
Основные методы для работы со store
Redux в функциональных компонентах (хуки)
Наше приложение будет построено при помощи функциональных компонентов и хуков, поэтому мы немного рассмотрим какие хуки предоставляет нам Redux для работы в таких компонентах.
. В документации редакса говорится о том, что лучше этот хук не использовать часто, а лучше пользоваться useSelector()
Теперь давайте вернемся к нашему приложению. В index.js нам нужно обернуть наше приложение в компонент Provider и передать ему store через пропсы. Store мы создали в файле store/store.js
Следующим шагом давайте создадим папку, в которой будем хранить наши компоненты и назовем ее componetns. В ней создадим 3 файла:
Для начала импортируем хук useDispatch(), т.к. в этом компоненте мы будем диспатчить 2 экшена: выполнение таска (toggleTask) и удаление (removeTask). В компоненте у нас есть событие onChange — при клике на этот компонент мы будем диспатчить экшен для переключения состояния таска, ну а при клике кнопки удалить — диспатчим экшен для удаления таска из нашего стора.
Далее файл src/components/TaskList.js
Теперь последний компонент AddNewTask.js
Тут у нас будет 2 обработчика handleTaskTitleChange() и handleTaskSubmit(). Последний будет диспатчить экшен для создания нового таска. В идеале, тут бы еще добавить проверку на пустую строку и обрезать лишние пробелы, но у нас не об этом сейчас 🙂
Теперь остался заключительный шаг, изменить файл scr/App.js и добавить немного стилей
В итоге у нас получилась вот такое приложение
Итоговое приложение
Redux: шаг за шагом
никаких деревьев, только камни
Redux стал одной из самых популярных реализаций идей Flux для управления потоком данных в приложениях на React. Однако в процессе изучения Redux часто возникает ситуация, когда «из-за деревьев мы не видим леса». Далее будет представлен простой и продуманный подход к приложениям, использующих Redux. На примерах мы шаг за шагом реализуем рабочее приложение, попытаемся применить принципы Redux на практике и обосновать каждое решение.
Продуманная методология для идиоматического Redux
Redux — это не просто библиотека. Это целая экосистема. Одна из причин его популярности — это возможность применять различные паттерны проектирования и подходы к написанию кода. К примеру, если мне нужно совершить некоторые асинхронные действия, то мне стоит использовать санки? Или может быть промисы? Или саги?
Какой подход верный? Единственного и четкого ответа нет. И нет «лучшего» пути использования Redux. Стоит признать, что большой выбор подходов заводит в тупик. Я хочу продемонстрировать свой личный вариант использования библиотеки. Он понятный, применимый к самым разнообразным сценариям из жизни и, что самое главное, он прост в освоении.
Итак, пора создать наше приложение!
Для продолжения нам нужен реальный пример. Давайте создадим приложение, показывающее самые популярные посты с Reddit.
На первом экране мы выясним у пользователя три наиболее интересные для него темы. После того, как пользователь сделает выбор, будем показывать список постов по выбранным темам (все посты либо посты по конкретной теме). По клику на пост в списке будем показывать его содержание.
Установка
Поскольку мы используем React, то для начала работы возьмем Create React App — официальный стартовый шаблон. Также установим redux, react-redux и redux-thunk. Результат должен быть похож на этот.
Давайте изменим index.js и создадим в нем стор, подключим санки:
Одна из главных вещей, которая часто упускается в различных Redux туториалах: а где же место Redux в этом цикле? Redux является реализацией Flux-архитектуры — паттерна для организации передачи данных в React-приложениях.
В классическом Flux для хранения стейта приложения используется стор. Диспатчинг ( передача) экшенов вызывает изменение этого стейта. После этого происходит перерендер представления в соответствии с измененным стейтом:
Flux упрощает разработку, создавая однонаправленный поток данных. Это уменьшает спагетти-эффект по мере роста кодовой базы приложения.
Одна из сложностей в понимании работы Redux — это множество неочевидных терминов типа редюсеров, селекторов и санков. Для более четкого понимания взглянем на расширенный Flux-цикл. Это просто различные Redux-инструменты:
Как вы могли заметить другие Redux-инструменты типа миддлваров или саг не показаны. Это сделано намеренно, эти инструменты не играют существенной роли в нашем приложении.
Файловая структура проекта
Создадим корневую папку /src и в ней следующие подкаталоги:
Папка store в свою очередь состоит из доменов, которые содержат:
State-first подход
Наше приложение имеет две стадии. На первой мы предлагаем пользователю выбрать три темы. Мы можем начать с реализации любого элемента Flux-цикла, но для себя я выяснил, что проще всего начать со стейта.
Итак, какой стейт приложения требуется для первой стадии?
Нам нужно будет сохранить список тем, полученных с сервера. Также нужно будет сохранить id выбранных пользователем тем (максимум три id). Будет нелишним сохранить порядок выбора. Например, если, в нашем случае, уже выбрано три темы и пользователь выбирает ещё, то мы будем удалять самую старую из выбранных тем.
Каким образом будет стуктурно организован стейт приложения? В моей предыдущей статье есть список полезных советов — Avoiding Accidental Complexity When Structuring Your App State. Руководствуясь этими советами, мы получим следующую структуру:
URL каждой темы будет служить уникальным id.
Я подготовил шаблон для создания редьюсера, вы можете посмотреть на него здесь. Обратите внимание, что для обеспечения иммутабельности нашего состояния (как того требует Redux), я выбрал библиотеку seamless-immutable.
Наш первый сценарий
Шаблон для создания умного компонента можно найти здесь. Также нам будет нужно вызвать его внутри корневого компонента App. Теперь, когда всё настроено, попробуем получить несколько тем с сервера Reddit.
Правило: умные компоненты не должны содержать никакой логики, кроме передачи действий (диспатчинг экшенов ).
Наш сценарий начинает работу с использования componentDidMount — метода представления. Исходя из правила выше, мы не можем запускать логику прямо из представления. Поэтому, для получения списка тем, мы будем диспатчить экшен. Это асинхронный экшен и он будет реализован с помощью санков.
Текущий исходный код нашего приложения можно увидеть здесь.
Несколько слов о сервисах
Как уже отмечалось ранее, сервисы используются для работы с внешним API, в большинстве случаев с сервер-API, как API Reddit. Плюс от использования сервисов в том, что наш код становится более независимым от изменений API. Если в будущем Reddit решит что-то изменить (конечную точку, названия полей), то эти изменения затронут только наши сервисы, а не всё приложение целиком.
Правило: cервисы должны быть stateless (то есть не должны иметь состояния).
На самом деле, это довольно неочевидное правило в нашей методологии. Представим, что случилось бы, если бы наше API требовало пароль. Мы могли бы сохранить стейт для логина с помощью данных для входа в систему внутри сервиса.
Однако наша методология разработки не позволила бы сделать это. Потому что весь стейт приложения должен находиться в сторе. Подходящим решением в этом случае будет передача каждого сервиса с данными для логина в виде аргумента и сохранения стейта логина в редюсере.
Реализация сервиса довольно проста, увидеть её можно здесь.
Завершение сценария — редюсер и представление
После того, как стейт обновился, мы должны вызвать перерендер представления. Это значит, что представление должно улавливать изменения той части стейта, от которого оно зависит. Это делается с помощью mapStateToProps.
Правило: умные компоненты должны обращаться к состоянию только с помощью селекторов.
Почему? Идея состоит в том, чтобы инкапсулировать внутренний стейт приложения и скрыть его от представления. Представьте, что позже мы решили изменить внутреннюю структуру. Без селекторов нам пришлось бы вносить изменения в каждый компонент представления, что нежелательно. Использование селекторов позволит проводить рефакторинг, изменяя только редьюсер.
Сейчас topics/reducer.js выглядит так:
Несколько слов о глупых компонентах
Глупые компоненты получают данные от родителя через пропсы и могут хранить локальный стейт.
Итак, когда же нам надо переходить от умного компонента к глупому?
Правило: вся логика представления в умных компонентах должна выноситься в глупые.
Если вы посмотрите на реализацию ListView, то вы можете увидеть некоторые скрипты представления, например итерацию строк. Мы должны избегать написания такой логики внутри умного TopicsScreen. Такой подход позволяет пользоваться умными компонентами только как прослойками. Другой плюс такого подхода в том, что ListView становится переиспользуемым.
Следующий сценарий — выбор нескольких тем
Первый сценарий завершен. Переходим к следующему: пользователь может выбрать только три темы из списка.
Важно отметить, что санку нужно получить доступ к состоянию. Также обратите внимание, что соблюдается правило, по которому доступ к стейту осуществляется через селектор.
Нам нужно будет обновить редюсер таким образом, чтобы он мог обрабатывать TOPICS_SELECTED и сохранять новые выбранные темы. Возникает вопрос, а должен ли selectTopic быть санком? Ведь мы можем сделать selectTopic простым объектом действия и передать его внутрь редюсера. Это тоже правильный подход. Лично я предпочитаю хранить бизнес-логику в санках.
Несколько слов о бизнес-логике
Один из принципов хорошей методологии является разделение представления и бизнес-логики. Где на данный момент у нас реализована бизнес-логика?
Правило: вся бизнес-логика должна находиться внутри обработчиков событий (санков ), селекторов и редюсеров.
Переход к следующей стадии — список постов
Когда у нас больше одного экрана в приложении, то нам нужна навигация. Зачастую для навигации используется react-router. Я сознательно избегаю маршрутизации, чтобы не усложнять наше приложение. Выбор внешних зависимостей, таких как маршрутизатор, часто отвлекает от основного процесса разработки.
Это все похоже на то, что мы уже делали ранее, за исключением одного: мы должны знать, когда именно показывать кнопку (как только будет выбрано минимум три темы). Возможно у вас возникнет желание добавить новую переменную для этих целей. Это избыточно: эквивалентное значение мы уже можем получить из данных, которые есть в стейте. Следовательно, эту часть бизнес-логики мы должны реализовать, как селектор.
Экран постов — снова state-first
Напоминание: задача второй стадии — отобразить список постов, которые можно отфильтровать в зависимости от выбранной темы. Пользователь может кликнуть на пост в списке и увидеть его содержание. Следуя обозначенной ранее структуре, получаем следующее:
И создаем новый редюсер здесь.
Первый сценарий — список постов без фильтрации
Наш стейт готов! Теперь реализуем упрощенную версию сценария без фильтра.
Это все очень похоже на то, что мы делали ранее. Реализация, по традиции, здесь.
Впрочем, ничего нового: реализация на месте.
Следующий сценарий — фильтр постов
Сценарий начинается с показа пользователю доступных фильтров. Мы можем получить данные, используя стейт редьюсера тем с помощью уже готового селектора. Когда фильтр будет выбран, мы диспатчим экшен, изменяющий стейт с помощью редюсера постов.
Полная реализация этого шага здесь.
Последний сценарий — содержание поста
Все готово
Код выше завершает реализацию нашего приложения. Полная версия приложения доступна на GitHub: https://github.com/wix/react-dataflow-example.
Какие выводы мы сделали:
Помните, что Redux предоставляет большое поле для экспериментов. Существуют подходы отличные от того, что использовали мы. У меня есть друзья, предпочитающие использовать redux-promise-middleware вместо санков и писать бизнес-логику только в редюсерах.
Если вы хотите поделиться своей собственной методологией для решения нашей задачи, не стесняйтесь: делайте PR в репозиторий проекта и мы рассмотрим его.
С 0 до 1. Разбираемся с Redux
Когда вышла версия 1.0 Redux, я решил потратить немного времени на серию рассказов о моем опыте работы с ним. Недавно я должен был выбрать “реализацию Flux” для клиентского приложения и до сих пор с удовольствием работаю с Redux.
Почему Redux?
Redux позиционирует себя как предсказуемый контейнер состояния (state) для JavaScript приложений. Редакс вдохновлен Flux и Elm. Если вы раньше использовали Flux, я советую прочитать, что Redux имеет с ним общего в разделе «Предшественники» новой (отличной!) документации.
Redux предлагает думать о приложении, как о начальном состоянии модифицируемом последовательностью действий (actions), что я считаю действительно хорошим подходом для сложных веб-приложений, открывающим много возможностей.
Конечно, вы можете найти больше информации о Redux, его архитектуре и роли каждого компонента в документации.
Создаем список друзей с React и Redux
Сегодня мы сфокусируемся на пошаговом создании вашего первого приложения, использующего Редакс и Реакт: создадим простой список друзей с нуля.
Вы можете найти готовый код здесь.
Для кого?
Эта статья написана для людей, не имеющих опыта работы с Redux. Опыт разработки с Flux также не обязателен. Я буду давать ссылки на документы, когда мы будем сталкиваться с новыми понятиями.
1. Установка
Автор Redux, Даниил Абрамов, создал отличную сборку для разработки с React, Webpack, ES6/7 и React Hot Loader, которую вы можете найти здесь.
Есть сборки уже с установленным Redux, но, я думаю, важно понять роль каждой библиотеки.
Теперь вы можете открыть приложение по адресу http://localhost:3000. Как вы видите, «hello world» готов!
1.1 Добавим redux, react-redux и redux-devtools
Нам нужно установить три пакета:
1.2 Структура директорий
Хотя то, что мы будем делать, довольно просто, давайте создадим структуру директорий как для реального приложения.
Мы будет видеть более детально роль каждой из директорий, когда будем создавать приложение. Мы переместили App.js в директорию containers, так что нужно будет настроить импорт statement в index.js.
1.3 Подключаем Redux
Нам нужно включить devtools только для окружения разработки, так что модифицируем webpack.config.js как здесь:
Мы делаем здесь две вещи. Мы переопределяем createStore используя созданную функцию, которая позволяет нам применять множественные store enhancers, таких как devTools. Мы также включаем функцию renderDevTools, которая рендерит DebugPanel.
Сейчас нам нужно модифицировать App.js для соединения с redux. Для этого мы будем использовать Provider из react-redux. Это сделает наш экземпляр хранилища доступным для всех компонентов, которые располагаются в Provider компоненте. Не нужно беспокоится о странно выглядящей функции, ее цель использовать “контекст” функции Реакта для создания хранилища, доступного для всех детей (компонентов).
Для создания хранилища мы используем createStore функцию, которую мы определили в devTools файле, как map всех наших редьюсеров.
В нашем приложении App.js — внешняя обертка для Redux и FriendListApp — корневой компонент для нашего приложения. После создания простого ‘Hello world’ в FriendListApp.js, мы можем наконец запустить наше приложение с redux и devTools. Вы должен получить это (без стилей).
Хотя это просто ‘Hello world’ приложение, у нас включен Hot Reloading, т.е. вы можете модифицировать текст и получать автоматическое обновление на экране. Как вы можете видеть, devtools справа показывает пустые хранилища. Заполним их!
2. Создаем приложение
Теперь, когда сделаны все настройки, мы можем сфокусироваться на самом приложении.
2.1 Действия и генераторы действий
Как вы можете видеть, это очень выразительный путь определения области действий нашего приложения, которое будет позволять нам добавлять друзей, отмечать их как «избранных» или удалять их из нашего списка.
Генераторы действий — функции, которые создают действия. В Redux генераторы действий являются чистыми функциями, что делает их портативными и простыми для тестирования, т.к. они не имеют сайд-эффектов.
Мы поместим их в папку действий, но не забывайте, что это разные понятия.
Как видите, действия довольно минималистичны. Чтобы добавить элемент, мы сообщаем все свойства (здесь мы имеем дело только с name), а для других мы ссылаемся на id. В более сложном приложении, мы, возможно, имели бы дело с асинхронными действиями, но это тема для другой статьи…
2.2 Редьюсеры
Мы, для начала, определяем вид состояния нашего приложения в initialState :
Состоянием может быть все, что мы захотим, мы можем просто сохранить массив друзей. Но это решение плохо масштабируется, так что мы будем использовать массив id и map друзей. Об этом можно почитать в normalizr.
Теперь нам нужно написать актуальный редьюсер. Мы воспользуемся возможностями ES6 для задания аргументов по умолчанию для обработки случаев, когда состояние не определено. Это поможет понять как записать редьюсер, в данном случае я использую switch.
Если вы не знакомы с синтаксисом ES6/7, то возможно вам будет трудно это прочесть. Т.к. нам нужно вернуть новое состояние объекта, как правило используют Object.assign или Spread operator.
Что здесь происходит: мы определяем новый id. В реальном приложении мы, возможно, возьмем его с сервера или, как минимум, убедимся, что он уникальный. Затем мы используем concat чтобы добавить этот новый id в наш id-лист. Concat добавит новый массив и не изменит оригинальный.
Как вы можете видеть, несмотря на синтаксис, который может сначала смутить, логика проста. Вы задаете состояние и получаете назад новое состояние. Важно: ни в одной точке этого процесса не изменять предыдущее состояние.
Окей, давайте вернемся и создадим редьюсеры для двух других действий:
Вы также можете заметить, что spread оператор позволяет нам манипулировать только теми состояниями, которое нам нужно изменить.
Redux не важно, как вы храните данные, так что можно использовать Immutable.js.
Теперь вы можете поиграть с хранилищем минуя интерфейс, путем вызова dispatch вручную в нашем App.js.
Вы увидите в devTools действия, с ними можно поиграть в реальном времени.
3. Создаем интерфейс
Т.к. этот урок не об этом, я пропустил создание React-компонентов и сфокусировался только на Redax. Мы имеем три простых компонента:
В Redux считается хорошей практикой делать по возможности большинство компонентов “глупыми”. Т.е. чем меньше компонентов связаны с Redux, тем лучше.
Здесь FriendListApp будет единственным “умным” компонентом.
Это часть нового синтаксиса ES7, называемая декоратор. Это удобный способ вызова функции высшего порядка. Будет эквивалентна connect(select)(FriendListApp); где select — функция, которая возвращает то, что мы здесь сделали.
То, что случится дальше — стандартный подход для React. Мы привяжем функции к onClick, onChange или onKeyDown свойствам, чтобы обработать действия пользователя.
Если вы заинтересовались, как это сделать, ты можете посмотреть весь код.
Сейчас вы можете почувствовать магию работы redux/react приложения. Как изображено на GIF, вы логгируете все действия.
Разрабатывать удобней, когда ты можешь производить какие-то действия, находить баги, возвращаться, исправлять их и повторять уже исправленную последовательность…