React урок 3 Архитектура приложения

В этой статье мы рассмотрим как структурировать сложный пользовательский интерфейс, выполненный из вложенных компонентов, рассмотрим потоки данных, зачем нужен propTypes и многое другое.

Проверка props

При создании компонентов, помните, что они могут быть составлены в более крупные компоненты и повторно использованы. Таким образом, хорошей практикой будет указать какие типы имеют реквизиты компонента. Это можно сделать путем объявления propTypes. Что нам это даст:

  • Документирование типов реквизитов;
  • Если все же мы перепутаем и укажем не тот тип, React нас предупредит сообщением в консоли.

Рассмотрим на примере:

Этот компонент принимает атрибут salutation и имеет тип строки, давайте укажем тип в коде явно и сделаем его обязательным:

Если убрать атрибут salutation, в консоле появиться предупреждение:

type_required

Значения по умолчанию

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

Для этого давайте сначала сделаем атрибут salutation необязательным удалив  isRequired и добавим значение по умолчанию:

Теперь если не указан атрибут salutation ему будет присвоено значение по  умолчанию «Hello World».

Типы PropTypes

В PropTypes определен набор валидаторов:

JS примитивы 
PropTypes.array массив
PropTypes.bool true/false
PropTypes.func функция
PropTypes.number число
PropTypes.object объект
PropTypes.string строка
 Комбинированные типы
PropTypes.oneOfType Любой из типов, например:

PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.instanceOf(Message)
])

PropTypes.arrayOf Массив определенного типа, например:

PropTypes.arrayOf(PropTypes.number)

PropTypes.objectOf Объект определенного типа, например:

PropTypes.objectOf(PropTypes.number)

PropTypes.shape Объект определенной формы,  например:

PropTypes.shape({
color: PropTypes.string,
fontSize: PropTypes.number
})

Специальные типы
PropTypes.node Может быть любым из: число, строка, элемент или массив.
PropTypes.element Элемент
PropTypes.instanceOf Значение должно быть экземпляром данного класса: PropTypes.instanceOf(Message)
PropTypes.oneOf Перечисление возможно одно из значений PropTypes.oneOf([‘News’, ‘Photos’])

Канбан доска: PropTypes

Давайте добавим типы в наши компоненты:

Пользовательские валидаторы

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

Канбан Доска: Создание пользовательского валидатора

Давай создадим валидатор для поля title компонента Card. В нем мы будем проверять title на длину если она превышает 80 символов будем возвращать ошибку:

custom_validator

Лучшие практики составления компонентов

Здесь мы рассмотрим лучшие практики создания React приложений путем композиции компонентов.

До сих пор мы видели что компоненты получают и управляют данными через реквизиты и состояние.

  • Реквизиты по сути это конфигурация компонентов. Компонент получат из через атрибуты от родительского компонента.
  • Состояние начинается со значений по умолчанию определенного в конструкторе, а затем изменяемые в основном через события генерируемых пользователем. Компонент сам управляет своим состоянием и каждый раз когда оно изменяется вызывается рендеринг компонента.

В React компонентах состояние не является обязательным. На самом деле, в большинстве приложений компоненты разделены на два типа: те у который есть состояние, а также те, которые не имеют внутреннего состояния и имеют дело только с отображением данных.

Чистые призваны только отобразить данные, что упрощает из тестирование и повторное использование.

Тем не менее, нам иногда нужно реагировать на действия пользователя, на ответ сервера.  Для этого нам нужно изменять состояние. Компоненты с внутренним состоянием, как правило выше по иерархии компонентов.

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

Какие компоненты должны иметь состояние?

По началу бывает сложно определить какие компоненты должны иметь состояние, здесь мы рассмотрим список рекомендаций:

  • Определите возможные компоненты, которые будут измениться при каком либо действии пользователя.
  • Найдите общего владельца этих компонентов.
  • Общий владелец или другой компонент выше по иерархии должен иметь состояние.
  • Если вы не можете найти компонент для объединения и содержания состояния, создайте его специально для этих целей.

Давайте рассмотрим эти принципы на примере, создадим простое приложение контактов:

contact_app

Иерархия компонентов

ContactsApp: Главные компонент

SearchBar: Поиск по контактам

ContactList: Список контактов

ContactItem: Контакт

Данные списка контактов будут храниться в глобальной переменной. В реальном приложении данные вероятно будут загружены удаленно, но для простоты они будут описаны в коде:

Сейчас все компоненты являются «чистыми» они только отображают данные. Тем не менее нам надо реализовать работу фильтра. Давайте пройдемся по контрольному списку, что бы определить где мы будет хранить состояние.

  1. Какие компоненты будут изменять свое представление при фильтрации?

Это SearchBar, он будет отображать текст поиска и ContactList ответственный за отображение списка.

2. Общий владелец это — ContactsApp, значит он и будет содержать состояние.

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

Потоки данные и коммуникация компонентов

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

Для начало создадим обратный вызов для изменения состояния:

Теперь давайте добавим обработку переданного обратного вызова в SearchBar:

Теперь все готово, фильтр работает

contact_app_done

Жизненный цикл компонента

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

Жизненный цикл этапы и методы

Монтирование компонента

mounting

componentWillMount — вызывается один раз, до вызова рендеринг. Важный момент, изменение состояния внутри данного метода не вызывает повторный рендеринг.

render — отвечает за рендеринг компонента, с этим методом мы уже знакомы.

componentDidMount — выполняется один раз, после рендеринга. На данный момент компонент имеет представление в DOM.

Удаление компонента

unmound

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

Изменение реквизитов

chage_props

componentWillReceiveProps —  Вызывается когда компонент получает новые реквизиты. Вызов this.setState() внутри метода, не вызовет повторный рендеринг.

shouldComponentUpdate — Специальный метод вызываемая перед функцией render, который дает возможность определить, требуется перерисовка или может быть пропущена. Что полезно для оптимизации производительности и будет подробно рассмотрено в 9 уроке.

componentWillUpdate — Вызывается непосредственно перед тем, как новые реквизиты или состояние получено. Любые изменения состояния через this.setState() не допускаются, так как эта функция должна строго использоваться для подготовки к предстоящему обновлению и не вызывать обновление.

componentDidUpdate — Вызывается сразу после обновления компонента когда изменения отобразились в DOM.

Изменение состояния

state_changes

Изменение состояний имей почти такой же жизненный цикл как у реквизитов, за одним исключением: там нет аналога метода componentWillReceiveProps. Если вам необходимо выполнить операции в ответ на изменение состояния, используйте componentWillUpdate.

Функции Жизненного цикла на практике: выборка данных

Чтобы проиллюстрировать использование методов жизненного цикла на практике, представьте, что вы хотите получать данные контактов удаленно. Поскольку эта глава посвящена стратегии и надлежащей практики для композиции компонентов, так же стоит отметить, что стоит избегать добавления выборки данных к компоненту, который уже имеет другую обязанность. Хорошая практика в место этого является создание нового компонента, единственная ответственность которого обмениваться данными с удаленным API, а также передача данных и функции обратного вызова в низ в качестве реквизитов. Некоторые называют такой компонент контейнером. Мы будем использовать идею компонента контейнера в нашем приложении Контакты. Для этого мы создадим новый компонент под названием ContactsAppContainer поверх ContactsApp.

Примечание: В следующем примере мы будем использовать новую функцию window.fetch, которая является более простым способом сделать запрос к серверу, чем XMLHttpRequest. Она может не поддерживаться в старых браузерах поэтому лучше использовать polyfill библиотеку из npm.

npm install —save whatwg-fetch

Давайте начнем с перемещения жестко закодированных данных в файл в формате JSON.

contacts.json

Теперь давайте создадим новый компонент:

Добавился только новый компонент, а остальные компоненты остались без изменения, плюс изменился рендеринг в конце листинга, теперь главным компонентом стал наш новый компонент ContactsAppContainer.

Немного поговорим об неизменяемости

Как вы уже знаете в React есть метод setState для изменения внутреннего состояния компонента. Будьте осторожны не используйте this.state на прямую. Существует ряд причин почему так не стоит делать. С одной стороны, путем манипулирования this.state непосредственно вы обходите механизмы управления React, что работает не только против парадигмы React, но и может быть опасно, потому что вызов setState() впоследствии может затереть ваши изменения. Более того манипулирование this.state сводит к минимуму возможные будущие улучшения производительности.

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

Неизменяемость в простых компонентах

Основная идея неизменяемости в замене объекта, а не в его изменении. Если вы не будите осторожны, вы можете не преднамеренно мутировать объекты, в место того, чтобы заменить их. Например, предположим, что у вас компонент с состоянием, который отображает данные о ваучерах на поездки авиакомпании (метод визуализации опущен в данном примере, потому что мы исследуем только состояние компонента):

Теперь предположим, что вы захотели добавить пассажира в массив пассажиров. Если вы не будите осторожны, вы можете непреднамеренно мутировать состояние компонента непосредственно, например:

Проблема в этом коде, как вы уже догадались, в том, что в JS объекты и массивы передаются по ссылке. Это означает, что когда вы говорите updatedPassengers=this.state.passengers, вы не передаете копию массива, вы получаете ссылку на массив. Кроме того, при помощи метода push, вы мутируете массив пассажиров. Что бы такого не происходило, надо использовать методы возвращающие новый массив или объект. Например так:

Здесь с помощью метода concat мы получили совершенно новый массив с новым пассажиром.

Есть также альтернативы для создания новых объектов без с мутациями в JS Object.assign.

Object.assign(target, source_1, …, source_n)

Сначала он копирует все перечисленные свойства к target из source_1, затем из source_2 и так далее.

Например, чтобы изменить flightNo, можно сделать так:

Примечание: Object.assign, является новым методом и не поддерживаться старыми браузерами, но хорошая новость в том что Babel (компилятор ES6 который мы используем вместе с Webpack) уже обеспечивает polyfill для старых браузеров.

Вложенные объекты

Хотя использование Object.assign может помочь, но в случае с вложенными объектами и массивами этот метод уже не кажется таким привлекательным.

Давайте посмотрим на примере:

Если мы изменим flightNo с помощью Object.assign:

update_flightNo

Как видно все хорошо, значение поменялось.

Однако, если изменить newTicket.arrival.airport=»MCO» в новом объекте, это же изменения отобразиться в оригинальном объекте.

arrival_chenge

Все потому что объекты и массивы передаются по ссылке.

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

React Immutability Helper

react-addons-update пакет обеспечивает функции обновления данных позволяющие избежать мутации данных.

Для начала, давайте его установим:

npm install –save react-addons-update

Затем можно импортировать:

import update from ‘react-addons-update’;

Метод обновление принимает два параметра. Первый объект или массив, который вы хотите обновить. Второй параметр является объектом, который описывает, где мутация должна происходить и какого рода мутации вы хотите сделать.  Давайте посмотрим на библиотеку в деле, предположим у нас есть простой объект описывающий студента:

что бы создать копию этого объекта добавлением нового элемента в массив grades, можно сделать так:

Этот объект {grades:{$push: [‘A’]}} описывает действия, поле grades — это место где мы хотим что-то сделать, $push это команда добавить в массив новый элемент. Если мы захотим изменить массив полностью, то можно использовать $set в замен $push:

А еще библиотека позволяет не обращать внимание на глубину вложенности свойств объекта, давайте посмотрим на примере с которым у нас уже были проблемы:

информацию которую мы хотим изменить — airport, который достаточно глубоко засел, вот таким не хитрым способом можно его изменить:

arrival_chenge_update

Индексы массивов 

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

array_change

Доступные команды

$push — добавляет в конец массива один или несколько элементов.

$unshift — добавляет в начало массив одни или несколько элементов.

$splice — аналог splice в JS:

$set — заменяет значение полностью.

$merge — объединяет заданный объект с изменяемым:

$apply — это функция в которую передается изначальное значение, вы что с ним делаете и возвращаете изменения в качестве результата.

Канбан доска: Добавим (немного) сложности

Для того, чтобы получать данные приложения на мы будем использовать удаленные запросы к API.

Получение данных карт от внешнего источника

Начнем с создания нового компонента в верхней части иерархии. Этот компонент контейнер будет использоваться для получения данных. Создадим новый файл KanbanBoardContainer.js

Для запросов данных мы будем использовать window.fetch и для того что бы он работал в старых браузерах рекомендую установить polyfill:

npm install —save whatwg-fetch

Для удобства получения данных можно воспользоваться http://kanbanapi.pro-ract.com.

Давайте начнем:

Вот мы и создали новый компонент контейнер, который извлекает данных удаленно. Теперь надо изменить главный компонент в файле App.js:

Если сейчас запустить проект, мы получим данных уже удаленно.

Методы управления даными

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

Теперь надо передать taskCallbacks в низ по иерархии компонентов:

Манипулирование задачами

А теперь давайте добавим реализацию в наши методы манипуляции. Есть одна проблема: при разбиении исходного массива cards на группы по текущему состоянию, мы теряем изначальный индекс в массиве. Но можно воспользоваться методом findIndex, который поможет найти исходный индекс.

метод findIndex может не поддерживаться в старых браузерах, так что советую установить babel-polyfill

npm install —save babel-polyfill

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

Теперь настало время toogleTask:

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

Базовая оптимизация работы с удаленным серверам

Когда пользователи, что-то делает он не хочет ждать, все должно работать мгновенно, и его не волнует что надо сходить на сервер, что-то туда положить и т.д. Потому мы с начало применяем изменения для клиента он видит их сразу же, а затем отправляем изменения на сервер. Но что произойдет если сервер выйдет из строя? По хорошему, надо сделать несколько попыток отправки. И если все же не удалось достучаться до сервера, хорошо бы было вернуться к предыдущему состоянию.

План такой, мы сохраняем начальное состояние и в случаи ошибки возвращаем все как было, что справедливо для всех трех методов:

Тут мы сохранили предыдущее состояние, а теперь добавим обработку ошибок от сервера:

Что бы проверить, можно локально выключить доступ в Интернет и попытаться сделать какие-либо изменения.

Полный листинг компонента KanbanAppContainer:

Итоги

В этой главе мы узнали, как построить комплекс UIs с React. Мы узнали, что в React приложении данные текут всегда в одном направлении, от родителя к ребенку. Для связи, родитель может передавать функцию обратного вызова ребенку и с помощью нее ребенок может отчитаться о каких либо изменениях. Еще мы узнали, что компоненты гораздо проще применять повторно, если разделить их на две категории: динамические компоненты (с состоянием) и чистые компоненты (без состояния). Это хорошая практика, чтобы динамических компонентов было меньше чем чистых компонентов. И как правило динамические компоненты находятся в верхней части иерархии компонентов. И наконец мы узнали почему важно не допустить мутации данных при обновлении состояния и узнали о неоценимой помощи библиотеки react-addons-update в работе с вложенными объектами.