react redux пример приложения

Введение в Redux & React-redux

image loader

Оглавление

Введение

Вот вы прочитали мою статью про 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:

Скорее всего вы используете папку 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:

2.2 reducer()

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

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

Функция принимает значение текущего состояния и обьект события (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 поэтому обновление состояния в моём случае выглядит так:

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

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

2.4 actionCreator()

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

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

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

2.5 Actions

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

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

2.6 getState()

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

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

2.7 subscribe()

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

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

2.8 combineReducers()

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

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

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

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

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

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

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

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

2.9 initialState

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

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

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

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

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

3. React-redux

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

3.1 Provider

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

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

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

3.2 mapStateToProps()

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

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

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

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

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

3.3 mapDispatchToProps()

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

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

Теперь для обновления свойства 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 для каждого компонента:

3.4 connect()

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

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

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

Источник

Redux: шаг за шагом

1*Df4RVgmo9sbrSIQrPo Piw

никаких деревьев, только камни

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 для хранения стейта приложения используется стор. Диспатчинг ( передача) экшенов вызывает изменение этого стейта. После этого происходит перерендер представления в соответствии с измененным стейтом:

0*XAjAw47PJTO4emNM

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

Одна из сложностей в понимании работы Redux — это множество неочевидных терминов типа редюсеров, селекторов и санков. Для более четкого понимания взглянем на расширенный Flux-цикл. Это просто различные Redux-инструменты:

0*oJIefYyj8aEXyMd9

Как вы могли заметить другие 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 в репозиторий проекта и мы рассмотрим его.

Источник

Понравилась статья? Поделить с друзьями:
Добавить комментарий
  • Как сделать успешный бизнес на ритуальных услугах
  • Выездной кейтеринг в России
  • Риски бизнеса: без чего не обойтись на пути к успеху
  • react native создание мобильного приложения
  • react native примеры приложений