React is a javascript library for building user interfaces
Так характеризуют Реакт его создатели, разработчики компании Facebook. Появившись в 2013 году, Реакт быстро стал набирать обороты и получил широчайшее распространение. На момент создания курса на Гитхабе у проекта более 70 тысяч звезд.
Секрет успеха в том, что Реакт позволил под другим углом посмотреть на процесс создания интерфейсов. Он резко снизил порог входа и сложность получаемых решений. Причем не только по сравнению с ручной работой с ДОМом, но и по сравнению со многими фреймворками.
И хотя Реакт как библиотеку для отрисовки можно встраивать в существующий технологический стек там, где это имеет смысл, он так же способен взять на себя полное управление фронтендом. Правда, в данном случае для эффективной работы придется подключить еще некоторые ключевые дополнения, такие как redux
и react-router
.
Фундаментальная идея, лежащая в основе работы Реакта, оказалась настолько мощной, что ее расширили далеко за рамки браузера. С Реактом можно работать как на сервере (server-side rendering), так и на мобильных платформах (React Native). Вы не ослышались: сейчас на языке JavaScript можно создавать приложения под мобильные платформы, которые работают почти так же эффективно, как и нативные приложения. Такую ситуацию, когда один подход используется для реализации разных задач (сайт, мобильные приложения) называется “Learn once, Write anywere” .
Практика
В этом курсе мы плотно пройдемся по возможностям Реакта и, я надеюсь, вы хорошо с ним разберетесь. Однако это не отменяет необходимости работать с Реактом и вне среды Hexlet.
CodePen
Самый простой способ попрактиковаться с Реактом – это сервис codepen. После регистрации вы сможете создать pen — изолированную среду разработки, подключив туда Реакт. Результаты кода отображаются там же, в соседней панели.
Сodepen позволяет вставлять пены прямо в свой сайт, чем я и буду пользоваться для демонстрации. Вы можете не только проанализировать такой код, но запустить и даже поправить его.
https://codepen.io/hexlet/pen/xLaegY
create-react-app
Разработчики в Фейсбуке, понимая как сложно настроить с нуля экосистему для старта фронтенд проектов, создали проект под названием create-react-app
. Это npm-библиотека, которая позволяет стартануть с нулевой конфигурацией:
npm install -g create-react-app
create-react-app my-app
cd my-app/
npm start
Дальше просто открывайте localhost:3000
и наслаждайтесь.
babel-preset-react
Если вы все же решитесь делать все самостоятельно, то не забудьте подключить пресет (preset) babel-preset-react
к вашей конфигурации Babel. Реакт расширяет JS и не может работать с Babel без этого пресета.
Курс
На протяжении всего курса мы будем создавать маленькие и не очень маленькие компоненты Бутстрапа. Если вы еще не знакомы с ним, то прочитайте наш гайд по Бутстрапу. В любом случае в каждой задаче будет подробно описано какой компонент использовать и как он должен выглядеть.
Как и в курсе JS: DOM API, большая часть тестов основана на снепшот-тестировании, а это значит, что важно использовать верстку именно так, как указано в задании.
Кроме этого, практически все задания визуализированы, и перед тем как запускать тесты убедитесь, что все работает в веб-доступе.
Отладка
Так как Реакт отрабатывает на фронтенде, то и ошибки будут появляться там же. Не забывайте всегда держать открытой консоль (например, в developer tools в Хроме) и внимательно читать все, что там написано. Большая часть ошибок будет выводиться именно там.
Также не забудьте поставить React Developer Tools . Это расширение для браузера, которое дает очень удобную панель для анализа происходящего с Реактом в вашем приложении. Начните его использовать с первого урока.
Поехали!
Сразу начнем с примера, который будем разбирать в течение урока:
See the Pen <a href=“https://codepen.io/hexlet/pen/xLaegY/”>js_react_component</a> by Hexlet (<a href=“https://codepen.io/hexlet”>@hexlet</a>) on <a href=“https://codepen.io”>CodePen</a>.
Центральное понятие в Реакте - компонент. Более того, это единственная сущность, которую он содержит. Вся остальная функциональность построена вокруг компонентов.
В примере выше создан компонент, который добавляет в DOM на странице <div>Hello!</div>
.
Вот как выглядит получившийся html:
<div id="react-root">
<div>Hello!</div>
</div>
Импорты
CodePen
импортирует Реакт автоматически, но в своем коде импорты пропускать нельзя:
import React from 'react';
import ReactDOM from 'react-dom';
Из кода и импортов видно, что для работы с Реактом нужно две библиотеки: сам Реакт и ReactDOM. Причина наличия двух зависимостей достаточно проста. Сама библиотека React
не связана с DOM напрямую и используется не только в браузере. Поэтому отрисовка конкретно для DOM вынесена в отдельный пакет ReactDOM.
Компонент
export default class Hello extends React.Component {
render() {
return <div>Hello</div>;
}
}
Очевидные тезисы
- Компонент Реакта – это класс, который наследуется от класса
React.Component
(как мы увидим позже, это не единственный способ создать компонент). - Функция
render
возвращает нечто (обсудим позже), что будет отрисовано в браузере. Класс-компонент без функцииrender
существовать не может, это его интерфейс.
Экспорт класса по умолчанию задан не спроста. В JS принято создавать класс на файл. В отличие от обычных классов, Реакт-компоненты имеют расширение JSX, а значит компонент, определенный выше, должен лежать в файле с именем Hello.jsx
.
Обратите внимание: класс все равно проименован, хотя это и не обязательно в случае дефолтного экспорта. Мы действительно можем его не именовать, но тогда в React Dev Tools
будет тяжело понять, что же отрисовал React
, так как любой безымянный компонент отображается как <ReactComponent>
. Поэтому возьмем себе за правило всегда давать компонентам имена.
Неочевидные тезисы
Самое поразительное происходит в этой строчке:
<div>Hello</div>;
Здравый смысл подсказывает, что такая запись синтаксически невозможна в JS. И он будет прав. То, что вы видите, называется JSX и является расширением языка (добавляется с помощью Babel). Кардинальное решение для фреймворка, не правда ли? В процессе вы поймете, что это не такая уж и плохая идея.
Главное сейчас запомнить то, что в конечном итоге любой компонент Реакта возвращает кусок DOM (на самом деле – virtual DOM).
Кстати, div
– это тоже компонент Реакта, только встроенный. Отличить встроенные компоненты от самописных очень легко. Встроенные всегда начинаются с маленькой буквы, а те, которые не являются частью Реакта, должны начинаться с большой.
Хорошим стилем считается давать расширение .jsx
для всех файлов, которые содержат JSX, независимо от того, создается ли компонент в этом файле или нет.
Mount
const mountNode = document.getElementById('react-root');
ReactDOM.render(<Hello />, mountNode);
Созданный компонент (класс компонента) сам по себе ничего не делает. Чтобы насладиться результатом его работы нужно произвести так называемое монтирование. То есть указать Реакту, куда его вставить в DOM.
Для этой задачи обязательно требуется реальная DOM-нода (узел), к которой и производится монтирование строчкой:
ReactDOM.render(<Hello />, mountNode);
Первым параметром передается наш компонент в синтаксисе jsx, а вторым та самая нода. Подходящей нодой может быть любой узел внутри body
. Как правило, если у нас не SPA, то React
используется в виде виджетов, подключаемых на странице в разных местах. Причем на одной странице может быть сразу несколько виджетов. Например, на Хекслете все фронтенд-элементы – это как раз виджеты.
JSX
JSX – это xml-like расширение js, созданное специально для задач Реакта. React из коробки поставляется с набором компонентов, которые полностью повторяют html. По большей части синтаксис и структура jsx и html совпадают, но есть некоторые важные различия:
- Так как это xml-like синтаксис, одиночные теги в jsx должны быть закрыты:
<hr />
. - Вместо свойства
class
в jsx используется имя свойства в DOM:className
.
Существуют и другие различия, о которых мы поговорим в следующих уроках. Большинство этих отличий делает работу с DOM внутри Реакта проще и удобнее.
Так же как и в html, из компонентов можно строить композиции, например такую:
const vdom = <div className="card">
<div className="card-body">
<h4 className="card-title">Card title</h4>
<p className="card-text">my text</p>
<a href="#" className="btn btn-primary">Go somewhere</a>
</div>
</div>;
И это все валидный код на JS с подключенным расширением для jsx.
То, что каждый компонент Реакта возвращает кусок DOM, является следствием его фундаментальной идеи и архитектуры. В одном из уроков мы рассмотрим эту идею подробнее и я уверен вы проникнитесь ей. Но почему понадобилось вводить jsx?
Нужно понимать, что jsx – расширение языка, а значит это именно код , а не html
. А раз jsx транслируется в код, то, следовательно, мы могли бы сразу писать этот код. Верно? Верно, но не совсем:
React.createElement(
"div",
{ className: "card" },
React.createElement(
"div",
{ className: "card-body" },
React.createElement(
"h4",
{ className: "card-title" },
"Card title"
),
React.createElement(
"p",
{ className: "card-text" },
"my text"
),
React.createElement(
"a",
{ href: "#", className: "btn btn-primary" },
"Go somewhere"
)
)
);
Пример кода выше – это как раз то, как бы выглядели функции render
компонентов на Реакте. Причем данный пример очень тривиальный и не содержит логику. Если бы у нас появились условные конструкции, то этот код перешел бы все разумные пределы по сложности анализа. К сожалению, или к счастью, собирать древовидные структуры в коде (а DOM – это дерево) – занятие очень тяжелое и беспощадное. Надеюсь, теперь стало чуть понятнее, зачем нужен jsx, и что jsx – это не верстка (как думают некоторые).
JS: React → JSX
С одной стороны, jsx – это такая же простая вещь как и голый html. Нужно запомнить как он собирается, и всё. С другой стороны, он встроен в сам JS и может с ним взаимодействовать. Другими словами, мы получили шаблонизацию прямо в языке программирования (да-да, так работает php). Именно это смешение с JS вызывает больше всего вопросов у новичков. Попробуем с ними разобраться.
Для лучшего понимания происходящего, проверьте во что транслируется код этого урока в онлайн компиляторе https://babeljs.io/repl/
Любой текст, записанный внутри тегов (будем называть их так для простоты), остается просто статическим текстом на выводе. А что делать, если нужно вставить значение переменной? Ответ ниже:
const name = 'Eva';
const cname = 'container';
const vdom1 = <div>Hello, {name}</div>;
const vdom2 = <div>Hello, {name.repeat(3)}</div>;
const vdom3 = <div className={cname}>Hello!</div>;
Как видно, вставка (по сути – интерполяция) происходит за счет использования фигурных скобок, причем внутри них может быть любое выражение. Эта схема работает одинаково как для содержимого тегов, так и для аттрибутов.
Кроме того, сами элементы jsx являются выражениями, то есть мы можем использовать их в любых местах JS-кода, которые работают с выражениями:
const name = 'Mike';
const vdom = block ? <div>hello, {name}</div> : <span>i am span</span>;
Теперь давайте соберем все вместе. Сам jsx – это выражение, а чтобы встроить выражение на JS внутрь jsx нужно использовать фигурные скобки. Следовательно, мы можем встроить jsx внутрь jsx пока мы пишем jsx:
const vdom = <div>
{isAdmin ? <p><a href="#">{text}</a></p> : <p>{text}</p>}
<Hello />
</div>;
Другими словами, jsx, как и любой язык программирования, имеет рекурсивную структуру. Мы можем вкладывать одни выражения в другие до бесконечности. В этом нет ничего удивительного, ведь jsx – это тот же код на JS, записанный особым образом.
Чтобы окончательно закрепить эту тему, давайте посмотрим на следующий код:
<div id={if (condition) { 'msg' }}>Hello World!</div>
Этот код не заработает по очевидной причине. Условная конструкция в JS - инструкция, а не выражение. В результате компиляции предыдущего кода получится:
React.createElement("div", {id: if (condition) { 'msg' }}, "Hello World!");
И это, вероятно, самое большое неудобство: невозможность использовать условную конструкцию внутри jsx. Хотя мы по-прежнему можем использовать тернарную операцию или, в более сложных случаях, делать так:
let button;
if (loggedIn) {
button = <LogoutButton />;
} else {
button = <LoginButton />;
}
return (
<nav>
<Home />
{button}
</nav>
);
Композиция
Как мы помним из предыдущего урока, все “теги” Реакта по сути являются встроенными компонентами, которые работают точно так же, как и определенные нами. А значит все, что применимо к самописным компонентам, также применимо и ко встроенным. Обратное тоже верно. На практике это означает, например, возможность комбинирования компонентов:
const vdom = (
<div>
<Hello />
<Hello />
<AnotherComponent>
<p>What is love</p>
</AnotherComponent>
</div>
);
В примере выше компоненты, записанные с заглавной буквы – самописные, остальные – встроенные. Это разделение не случайно: Реакт требует, чтобы вновь создаваемые компоненты начинались с большой буквы, что кстати соответствует стандарту именования классов в JS.
Null
В реальной практике возникают ситуации, когда наличие того или иного компонента в DOM зависит от некоторых условий. Например, если в компонент не передали текст, то и не надо выводить соответствующий кусок. Пример:
const header = text ? <h1>{text}</h1> : null;
const vdom = (
<div>
{header}
<Hello />
</div>
);
либо так:
const vdom = (
<div>
{text ? <h1>{text}</h1> : null}
<Hello />
</div>
);
То есть null
– это допустимое значение, которое рассматривается Реактом как пустой компонент. Точно также интерпретируются false
, true
и undefined
. Поэтому пример выше можно переписать еще короче.
const vdom = (
<div>
{text && <h1>{text}</h1>}
<Hello />
</div>
);
Комментарии
JSX не поддерживает комментарии напрямую, но их можно эмулировать используя JavaScript. Для этого достаточно вставить блок кода, внутри которого многострочный JavaScript комментарий.
{/* A JSX comment */}
То же самое для многострочного комментария:
{/*
Multi
line
comment
*/}
JS: React → Props
Компонент Card
, который мы писали ранее, на практике бесполезен, так как не позволяет поменять тексты. А создавать на каждый конкретный блок Card
свой собственный компонент — не самая хорошая идея. Я уже не говорю о том, что чаще всего такое просто невозможно, ведь данные подставляются динамически.
https://codepen.io/hexlet/pen/YxObvW
Передавать данные в компоненты можно, и делается это с помощью механизма props
:
Как видно, снаружи свойства передаются как аттрибуты в html, а внутри компонента доступны по свойству (то же самое слово, но обозначает другое) props
объекта. Причем такая передача свойств для нас уже не в новинку. Встроенные компоненты точно так же принимают на вход свойства, такие как className
и другие.
const vdom = <div className="row">
<div className="col-6">
<HelloMessage name="Kate" />
</div>
<div className="col-6">
<HelloMessage name="Mark" />
</div>
</div>;
Props — очень простой механизм передачи данных в компоненты, который, как правило, не вызывает никаких сложностей. Главное что нужно запомнить при работе с props: их нельзя изменять . Во-первых, из-за принципа работы Реакта это просто ни к чему не приведет, во-вторых, для работы с изменяемым состоянием в Реакте предусмотрен совершенно другой механизм, который мы рассмотрим позже.
Spread
Работая с props, нередко приходится передавать множество параметров, либо эти параметры присутствуют в коде в виде объекта. В таком случае можно упростить передачу используя механизм spread.
const params = {
className: 'row',
title: 'name',
}
const name = 'Eva';
const vdom = <div id="container" {...params}>
Hello, {name}
</div>;
Код выше эквивалентен следующему примеру:
const name = 'Eva';
const vdom = <div id="container" className="row" title="name">
Hello, {name}
</div>;
Default props
Другая задача, с которой сталкиваются разработчики - установка значений по умолчанию для props
. Проще всего устанавливать их прямо внутри функции render
используя такой подход:
const title = this.props.title || 'hi!';
Это сработает, но потенциально может привести к проблемам производительности (в первую очередь). Мы поговорим об этом в одном из последних уроков.
В реакте предусмотрен способ устанавливать значения props по умолчанию. Пример:
class Header extends React.Component {
static defaultProps = {
text: 'Hello, world!',
};
render() {
return (
<h1>{this.props.text}</h1>
);
}
}
JS: React → Работа с коллекциями
В работе с коллекциями элементов в jsx
по большей части нет ничего особенного. С другой стороны, задача обработки списков элементов настолько частая, что будет не лишним ее обсудить отдельно.
https://codepen.io/hexlet/pen/YxJaKG
Выше приведен типичный код, в котором коллекция генерируется прямо в том месте, куда и подставляется. Здесь мы снова видим, что внутрь jsx
вложено выражение (через {}
) внутри которого опять появляется jsx
. Как правило, рекурсия на этом заканчивается :). Если нужна более сложная обработка, то имеет смысл вынести генерацию коллекции в метод компонента и вызывать его внутри render
, например так:
class List extends React.Component {
renderList() {
// ...
}
render() {
return (
<ul>
{this.renderList()}
</ul>
);
}
}
Key
Для повышения эффективности Реакт настоятельно рекомендует идентифицировать каждую строку коллекции, которая генерируется. Связано это с механизмом, который производит изменения в DOM’е. Подробнее мы поговорим об этом позже, а сейчас нужно просто запомнить, что, генерируя коллекцию элементов в jsx
, нужно обязательно проставлять уникальное свойство key
, которое не меняется при повторной генерации коллекции.
Чаще всего с этой задачей не возникает проблем, так как у любой сущности, с которой мы работаем, есть свой идентификатор (например, primary key из базы данных).
class List extends React.Component {
render() {
const { data } = this.props;
return (<ul>
{data.map(item => <li key={item.id}>{item.name}</li>)}
</ul>);
}
}
Если подобного идентификатора нет, то можно использовать функцию uniqueId
из библиотеки lodash
.
import { uniqueId } from 'lodash';
class List extends React.Component {
render() {
const { data } = this.props;
return (
<ul>
{data.map(item => <li key={uniqueId()}>{item.name}</li>)}
</ul>
);
}
}
Такой подход сработает, так как требуется уникальность только среди соседей.
Как видите, ничего сложного в этом нет. Более того, если по какой-то причине вы забудете указать key
в коллекции, то React
начнет кидаться warning’ами об этом прямо в консоли браузера. Поэтому пытаться запомнить когда их ставить, когда нет - не надо. В процессе работы вы и так об этом узнаете и сможете легко поправить.
Кстати, key
не обрабатывается как обычные свойство и его нельзя получить внутри компонента так: this.props.key
. Если вам нужны данные, которые были переданы в key
внутри компонента, то просто передайте их отдельным свойством:
const content = posts.map((post) =>
<Post
key={post.id}
id={post.id}
title={post.title} />
);
Root
Раньше это было не так, но сейчас допустимо из компонента возвращать массив компонентов. Реакт сам правильно вставит их в DOM:
class List extends React.Component {
render() {
const data = this.props.data
const f = item => <div key={item.id}>{item.name}</div>;
return data.map(f);
}
}
JS: React → Различия jsx и html
Хотя jsx и пытается быть похожим на html, у них все же есть некоторые отличия.
В jsx все свойства DOM и аттрибуты (включая обработчики событий) должны быть записаны в camelCase . Например, аттрибут tabindex превращается в tabIndex . Исключением являются aria- и data- аттрибуты, они записываются точно так же, как и в обычном html.
htmlFor
Так как for — зарезервированное слово в js, в элементах реакта используется свойство htmlFor .
Экранирование
Обычный html не очень безопасен. Любой текст, который должен оставаться текстом, необходимо экранировать перед выводом. Иначе если внутри содержится html, то он будет проинтерпретирован. Ситуация может стать опасной, если этот текст на сайт добавляют сами пользователи.
jsx работает по-другому. Все, что выводится обычным способом - безопасно по умолчанию и экранируется автоматически. А вот в тех местах, где этого не требуется, экранирование отключается так:
https://codepen.io/hexlet/pen/MvLNZg
По сути, для вывода без экранирования нужно использовать свойство dangerouslySetInnerHTML .
Стили
Совсем по другому работает аттрибут style . Если в html это обычная строка, то в jsx только объект.
https://codepen.io/hexlet/pen/NvoQOe
Для консистентности с именами аттрибутов в html, имена свойств css также должны использовать camelCase .
Значение свойств по умолчанию
Если свойство передается в компонент без значения, то оно автоматически становится равным true
.
Примеры ниже эквивалентны:
<MyTextBox autocomplete />
<MyTextBox autocomplete={true} />
При этом предпочтительным является первый вариант.
Остальное
Более подробно о различиях можно прочитать в официальной документации. Кроме того, в будущих уроках мы будем сталкиваться с этими различиями на практике.
JS: React → Обработка имен классов
Интерактивные элементы UI имеют более одного состояния отображения. Например, модальное окно может быть открыто или закрыто, а переключатель включен или выключен. Общепринято менять эти состояния классами.
Работая напрямую с dom, можно использовать classList , который содержит удобные методы для добавления и удаления классов. В Реакте из коробки нет никаких удобств. Свойство className — всего лишь строка со всеми вытекающими последствиями.
class Button extends React.Component {
render () {
let btnClass = 'btn';
if (this.props.isPressed) {
btnClass += ' btn-pressed';
} else if (this.props.isHovered) {
btnClass += ' btn-over';
}
return <button className={btnClass}>{this.props.label}</button>;
}
};
Для решения этой задачи создатели Реакта рекомендуют использовать пакет classnames . Принцип его работы прост: вместо манипулирования строчкой напрямую нужно сформировать правильный объект, который уже будет преобразован в строку.
import cn from 'classnames'
class Button extends React.Component {
render () {
const btnClass = cn({
btn: true,
'btn-pressed': this.props.isPressed,
'btn-over': !this.props.isPressed && this.props.isHovered,
});
return <button className={btnClass}>{this.props.label}</button>;
}
};
Иногда имя класса генерируется динамически, тогда можно использовать следующий код:
const buttonType = 'primary';
const btnClass = cn({
[`btn-${buttonType}`]: true,
});
JS: React → Children
UI-элементы имеют иерархическую структуру. Например, компонент card в Бутстрапе:
<div class="card">
<img class="card-img-top" src="..." alt="Card image cap">
<div class="card-body">
<h4 class="card-title">Card title</h4>
<p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
<a href="#" class="btn btn-primary">Go somewhere</a>
</div>
</div>
Блок карточки может содержать внутри себя картинку и тело. Тело в свою очередь может состоять из заголовка и текста, а текст может быть чем угодно. Не удивлюсь, если в теле карты получится разместить другие карты.
То же самое применимо как к элементарным элементам самого html, например, тегу div , так и к остальным компонентам Бутстрапа, таким как модальные окна и навигация.
html соответствует природе ui и естественным образом позволяет строить композиции элементов за счет вкладывания тегов друг в друга. Точно так же работает и jsx. Пока мы использовали этот факт только для встроенных компонентов. Теперь пришла пора попробовать реализовать подобное поведение в самописных компонентах. Возьмем alert из bootstrap.
https://codepen.io/hexlet/pen/YxgWVm
В примере выше обязательной частью является только основной div . Содержимое зависит от конкретной ситуации. Подставляется оно с помощью свойства children .
https://codepen.io/hexlet/pen/GveqMp
Обратите внимание на то, что компонент стал использоваться как парный тег в jsx:
const vdom = <Alert>
<p>Whenever you need to, be sure to use margin utilities to keep things nice and tidy.</p>
</Alert>;
Все, что находится между открывающим и закрывающим тегом, попадает внутрь свойства children .
Но будьте бдительны: тип данных свойства children зависит от содержимого. В простейшем случае, когда тег используется как одиночный <div />
, это свойство будет равно undefined.
Если этим содержимым является строчка, то именно она окажется внутри children . Правда, после некоторой обработки. JSX удаляет концевые пробелы и пустые строки. Следующие примеры будут отображены одинаково:
<div>Hello World</div>
<div>
Hello World
</div>
<div>
Hello
World
</div>
<div>
Hello World
</div>
Любой одиночный дочерний компонент также будет представлен сам собой в children . Во всех остальных случаях children будет содержать массив.
Если внимательно посмотреть на документацию Реакта, то можно увидеть следующее определение children : “children are an opaque data structure” (свойство children - непрозрачная структура данных). Другими словами, нельзя однозначно полагаться на тип этого свойства, так как снаружи можно передать все что угодно.
Реакт предоставляет набор функций, предназначенных для манипулирования свойством children . Все они доступны в React.Children
.
https://codepen.io/hexlet/pen/ayMZrr
Точно так же, во избежании конфузов для определения количества элементов внутри children нужно пользоваться специализированной функцией Реакта. Например, у children со значением “Hello World!” длина будет 12. Совсем не то что мы ожидали.
class ChildrenCounter extends React.Component {
render() {
return <p>{React.Children.count(this.props.children)}</p>
}
}
// Renders "1"
<ChildrenCounter>
Second!
</ChildrenCounter>
// Renders "2"
<ChildrenCounter>
<p>First</p>
<ChildComponent />
</ChildrenCounter>
// Renders "3"
<ChildrenCounter>
{() => <h1>First!</h1>}
Second!
<p>Third!</p>
</ChildrenCounter>
Кроме перечисленного выше, бывает необходимо обработать детей перед выводом, изменив часть свойств. Конечно же, напрямую этого сделать нельзя, ведь свойства неизменяемы. Такого поведения можно добиться клонируя элементы функцией React.cloneElement
.
JS: React → Состояние
Мы основательно изучили JSX и неинтерактивный способ работы с компонентами Реакта. С этого урока начинается самая главная часть: взаимодействие с пользователем. Ключевые понятия, которые будут рассмотрены в этом уроке: события и состояние. Начнем с примера:
https://codepen.io/hexlet/pen/ZJPpog
Компоненты, которые мы создавали раньше, были stateless, то есть не содержали никакого состояния и могли только отрисовывать переданные свойства. Компонент в примере выше является stateful, так как сохраняет внутри себя состояние счетчика. По порядку:
- Внутри компонента определяется начальное состояние
state = { count: 0 }
, с которым будет инициализирован компонент после отрисовки. Единственное требование к состоянию, которое предъявляет Реакт - тип данных: он должен быть объектом. То, что хранится внутри, определяется самим приложением.Альтернативный (и эквивалентный) способ задания начального состояния выглядит так:
class Component extends React.Component {
constructor(props) {
super(props); // всегда обязательно
this.state = { count: 0 };
}
}
Обратите внимание на то, что это единственное место, где state может изменятся напрямую (точнее, создаваться). Во всех остальных местах this.state
должен использоваться только для чтения! (подробнее дальше)
2. Функция render
использует данные из state для отрисовки. Здесь никаких сюрпризов.
3. На кнопку вешается обработчик на клик. В отличие от html, в свойство onClick
передается функция, и она вызовется автоматически в момент срабатывания события. Внутри обработчика читается текущее значение счетчика, к нему прибавляется единица и далее идет установка нового состояния. Повторюсь: крайне важно не изменять state напрямую. Для установки нового состояния в реакте предусмотрена функция setState
. Именно ее вызов приводит к тому, что компонент, в конце концов, перерисуется. Происходит это не сразу, то есть setState
работает асинхронно и внутренняя магия пытается оптимизировать процесс рисования.
Еще один важный момент заключается в том, как определена функция onClick
. Так как мы работаем с классом, то логично было бы использовать такой стиль определения:
class Counter extends React.Component {
onClick() {
const count = this.state.count;
this.setState({ count: count + 1 });
};
}
Но такой подход плохо работает в Реакте по двум причинам.
Первое: обработчики вызываются асинхронно, а методы в классах — это обычные функции с поздним связыванием. Поэтому мы не можем просто так повесить обработчик, так как он потеряет this
. С таким определением придется постоянно писать подобный код: onClick={this.onClick.bind(this)}
либо такой onClick={() => this.onClick()}
.
Вторая причина связана с производительностью. Оба предыдущих примера передачи обработчика порождают при каждом вызове функции render
новые обработчики (так как функции сравниваются по ссылкам, а не по содержимому), а для Реакта это критично. Поэтому правильный способ определения - стрелочная функция:
class Counter extends React.Component {
onClick = () => {
const count = this.state.count;
this.setState({ count: count + 1 });
};
}
Подробнее о производительности поговорим позже.
По большому счету описанный выше механизм открывает практически все двери. Теперь вы с легкостью можете создавать интерактивные компоненты и оживлять ваш UI. Все остальное — это тонкости, предусмотренные для различных ситуаций.
Инициализация
Предположим, что в компоненте, созданном выше, нужно инициализировать счетчик свойством count
, переданным снаружи. И только в его отсутствие ставить 0. Для решения этой задачи нужно добавить две вещи:
- Использовать свойство
count
как начальное значение счетчика. - Добавить значение по умолчанию для свойства
count
.
https://codepen.io/hexlet/pen/ZJPeeZ
setState
Усложним компонент и реализуем две кнопки, каждая их которых управляет своим состоянием.
https://codepen.io/hexlet/pen/YxgZxN
В данном примере объект состояния включает два свойства: count
для одной кнопки и primary
для другой. Основная хитрость этого примера заключается в процессе обновления состояния:
// первая кнопка
this.setState({ count: count + 1 });
// вторая кнопка
this.setState({ primary: !primary });
Функция setState
не просто принимает на вход новое состояние. Она заменяет значения ключей в предыдущем состоянии на значения этих же ключей в новом состоянии. То что передано не было - не трогается. То есть, в нашем случае мы передавали только то, что изменяли. На практике это поведение крайне удобно, иначе пришлось бы каждый раз выполнять работу по слиянию старого состояния с новым руками.
Структура объекта состояния
Существует множество способов организации данных внутри состояния. Скорее всего, вы захотите хранить их как-то так:
const blogPosts = [
{
id : "post1",
author : {username : "user1", name : "User 1"},
body : "......",
comments : [
{
id : "comment1",
author : {username : "user2", name : "User 2"},
comment : ".....",
},
{
id : "comment2",
author : {username : "user3", name : "User 3"},
comment : ".....",
}
]
},
{
id : "post2",
author : {username : "user2", name : "User 2"},
body : "......",
comments : [
{
id : "comment3",
author : {username : "user3", name : "User 3"},
comment : ".....",
},
]
}
// and repeat many times
]
При таком подходе сущности зависимые от других, находятся внутри. Если брать пример выше то это означает что каждый пост содержит внутри себя как и автора, так и список комментариев, а каждый комментарий, в свою очередь, содержит внутри свои связанные сущности, того же автора. При таком подходе получается что состояние представляет из себя дерево зависимостей. Хотя такой способ организации кажется вполне естественным, работать с ним крайне тяжело. Во-первых одни и те же данные начнут дублироваться в разных местах и вам придется синхронизировать изменения в них, что создает космические проблемы на пустом месте. Во-вторых обновления таких данных (особенно в иммутабельном стиле) становятся сложными и многословными. В-третьих, так как все состояние это один большой кусок, то любое обновление приведет к его полному копированию, что может быть дорогой операцией (в зависимости от размера состояния и количества обновлений в единицу времени).
Общая рекомендация, которую дают разработчики Реакта, это делать структуру максимально плоской, похожей на то, как мы храним данные в базе данных. Причем, желательно, в хорошо нормализованном виде. Другими словами, не нужно дублировать данные в состоянии. Пример того как правильно:
const state = {
articles: [/*...*/],
comments: [/*...*/],
}
JS: React → События
На первый взгляд может показаться что в Реакте используются обычные браузерные события, но это не так. Реакт самостоятельно перехватывает все события, возникающие в DOM, и транслирует их во внутреннюю систему.
В любой обработчик события при вызове передается объект типа SyntheticEvent
, кроссбраузерный враппер (обертка) над нативным объектом события. Интерфейсно он не отличается от нативного, кроме того, что работает одинаково во всех браузерах.
class Component extends React.Component {
onClick = (event) => {
console.log(event); // => nullified object.
console.log(event.type); // => "click"
const eventType = event.type; // => "click"
}
// ...
}
Правда, в целях оптимизации вместо порождения нового объекта на новое событие, Реакт переиспользует старый объект. Значит, код ниже работать верно не будет:
class Component extends React.Component {
onClick = (event) => {
this.setState({clickEvent: event});
}
// ...
}
Поэтому правильно сохранять в стейте не сам объект события, а его свойства.
Как правило, само событие используется не часто. Например при кликах, обычно, важен сам факт клика, а не его параметры, такие как координаты места возникновения. С другой стороны, событие нужно часто для предотвращения действия по умолчанию. Действительно, если ничего не предпринимать, то после клика страница будет перезагружена. В этом смысле ничего нового. И без Реакта все работает так же. Ниже правильный способ обработки такой ситуации:
https://codepen.io/hexlet/pen/NvVXoy
В обычном html подобное поведение можно получить и другим способом. Для этого достаточно вернуть false
из обработчика. В Реакте такой вариант не пройдет.
Точно так же нужно поступать при необходимости предотвратить всплытие события. Только вместо preventDefault
вызывается функция stopPropagation
.
В курсе JS DOM я говорил, что при работе с HTML предпочтительно использовать addEventListener
. Одна из главных причин заключается в том, что такой способ позволяет повесить множество обработчиков, чем и пользуются многие JS дополнения. В React такой способ работы просто не нужен, так как управление потоком событий всегда явное. Никто не может подключиться к Реакту со стороны и навесить туда своих обработчиков.
Второй момент, который может пугать разработчиков, это навешивание обработчиков прямо в JSX. Не лишний раз будет напомнить, что JSX — это JS-код, а не HTML. Поэтому нет никакой проблемы. Как вы увидите позже, такой код очень просто читать, потому что все находится в одном месте.
React нормализует события так, что они имеют консистентные свойства в различных браузерах. Кроме того, в формах добавляется событие onChange
, которое ведет себя в соответствии со своим названием и сильно упрощает работу.
JS: React → Автоматное программирование
Тема конечных автоматов занимает центральную роль во фронтенд разработке. Интерактивные элементы всегда вовлечены в процессы, связанные с изменением состояний. Модальные окна бывают открытые и скрытые, кнопка нажата, отжата или заблокирована (например, во время ajax-запроса). Примеров бесконечное множество. Нередко эти автоматы зависят друг от друга, что порождает иерархию автоматов. Например, возможность взаимодействовать с элементом на экране может появляться только после нажатия кнопки “редактировать”.
В Реакте работа с автоматами проста до безобразия и в большинстве случаев не требует использования специальных библиотек. Возьмем, к примеру, кнопку, которая отвечает за показ куска текста. Ее состояния можно описать так:
- По умолчанию текст скрыт (состояние hidden)
- Клик по кнопке отображает текст (состояние shown)
- Повторный клик прячет текст (hidden)
В данном случае у кнопки два состояния, поэтому можно упростить задачу и использовать флаг как индикатор состояния. Назовем его как isShown
.
Я очень не рекомендую так делать в бекенде, когда состояние хранится в базе. Цена изменения автомата слишком высока, поэтому, даже, в случае бинарной логики, лучше делать полноценный автомат с именованными состояниями. Другими словами для хранения состояния используйте не булево поле (с true/false), а текстовое поле, в котором будет содержаться полное название состояния. Например, если статья может находиться в двух состояниях, Опубликована или Не опубликована, то нужно делать не поле ‘published: bool’ со значениями true и false, а поле ‘publishing_state’ со значениями ‘published’ и ‘unpublished’`
https://codepen.io/hexlet/pen/MvMYbr
Большая часть кода в Реакте (во всем фронтенде) выглядит именно так, как в примере выше. События порождают изменения состояния в данных, на основе которых, в свою очередь, меняется представление. Количество конечных автоматов во фронтенд приложениях растет с астрономической скоростью, главное их видеть и выделять явно.
Структура состояния
Данные, с которыми работает Реакт, как правило, приходят из бекенда. И эти данные тоже участвуют в разных процессах и находятся в разных состояниях. Например, статья может быть опубликована, а может быть и нет. И в зависимости от того, в каком она состоянии, рисуется UI. И здесь начинается самое интересное. Конкретно состояние опубликованности статьи не является частью UI, но UI использует это состояние, а при изменениях оно синхронизируется на фронтенде и бекенде. Но в UI часто появляются состояния, которые отвечают исключительно за внешний вид, но не являются частью данных.
Если предположить, что данные, пришедшие с бекенда, внутри нашего объекта-состояния хранятся как список под ключем items
, то возникает вопрос: куда записывать данные, отвечающие за состояние UI? То есть те самые стейты, которые появляются только при взаимодействии с пользователем и не используются на серверной стороне?
Поясню на примере. К нам приходит статья такой структуры: { id: 3, name: 'How to programm', state: 'published' }
. Мы отправляем ее в items
. А в UI есть возможность зайти в ее редактирование, и для этого используется флаг (состояние) isEditing
, который существует только на экране. Вопрос: где хранить эту переменную?
Самый простой вариант: изменить саму статью внутри items
, так, чтобы она имела такой вид: { id: 3, name: 'How to programm', state: 'published', isEditing: true }
. Хотя, на первый взгляд, он и кажется разумным, проблем он привносит больше, чем пользы. В основном эти проблемы связаны с задачами синхронизации. Иногда бывает нужно отправить всю статью на сервер (после изменений), а иногда перечитать ее заново с бекенда. В такой ситуации нужно будет либо извлекать только нужные данные, либо постоянно делать мердж (слияние), чтобы не потерять состояние UI. Практика показала, что гораздо проще добавлять отдельный список исключительно под задачи хранения состояния UI. То есть в нашем стейте появится список под названием itemUIStates
, и для нашей статьи в него добавится элемент { articleId: 3, isEditing: true }
.
JS: React → Формы
Формы в HTML работают немного не так, как формы в Реакте. Это связано с тем, что в HTML они имеют свое внутреннее состояние, место, в котором хранятся значения форм, тексты, выбранные опции и тому подобное.
<form action="">
<label>
Name:
<input type="text" name="name" />
</label>
<input type="submit" value="Submit" />
</form>
Форма выше при каждом изменении поля name изменяет свое внутреннее состояние, которое будет отправлено по нужному адресу при сабмите.
В отличие от прямой работы с DOM (даже через jQuery), в Реакте источником правды является состояние, а не DOM. Формы не являются исключением. Любое изменение в форме, посимвольно, если это ввод, должно быть перенесено в состояние. А элементы форм, чьи данные хранятся в стейте Реакта, называются “controlled components”.
https://codepen.io/hexlet/pen/zdVKJy
В коде выше, на каждое изменение в элементе input происходит извлечение содержимого через e.target.value
и запись его в Реакт. Последующий сабмит не нуждается в самой форме, так как все данные в стейте. Поэтому при отправке формы достаточно взять нужный стейт и отправить его, например, на сервер. Обратите внимание: наш элемент формы становится контролируемым (controlled components) только когда происходит подстановка его значения из Реакта: <input value={this.state.text} />
.
Один из множества плюсов контролируемых компонентов в том, что становится крайне легко проводить фильтрацию или валидацию. Например, если мы хотим чтобы данные вводились в верхнем регистре (например, при вводе данных карты), то сделать это можно так:
handleChange = (e) => {
this.setState({ value: e.target.value.toUpperCase() });
}
В противовес контролируемым компонентам Реакт позволяет использовать “uncontrolled component”. При таком подходе состояние формы хранится в самом DOM. Этот способ нужен исключительно для интеграции со сторонними библиотеками или для работы с легаси (устаревшим) кодом. В нормальной ситуации он не понадобится.
Textarea
В HTML значение <textarea>
устанавливается как его содержимое:
<textarea>
Like this
</textarea>
В Реакте для этого, используется аттрибут value
https://codepen.io/hexlet/pen/GvbqvP
Стоит отметить, что событие onChange
в Реакте работает так, как ожидается, в отличие от onChange
в html, который срабатывает только когда элемент теряет фокус. Поэтому мы гарантировано получаем срабатывание события на каждое изменение. При этом, данные из элемента формы извлекаются обычным способом, через e.target.value
. Ну, а дальше все по старой схеме — данные обновляются в стейте.
Select
В HTML текущий элемент выбирается с помощью аттрибута selected
, проставленного на нужном option
.
<select>
<option value="grapefruit">Grapefruit</option>
<option value="lime">Lime</option>
<option selected value="coconut">Coconut</option>
<option value="mango">Mango</option>
</select>
Реакт предлагает другой, более простой и удобный способ. Достаточно проставить аттрибут value
компонента select
в нужное значение.
https://codepen.io/hexlet/pen/brPgqP
checkbox & radio
Оба этих типа поддерживают аттрибут checked
. Если он выставлен, то элемент отмечается выбранным.
<input name="isGoing" type="checkbox" checked={this.state.isGoing} onChange={this.handleInputChange} />
Шаблонный код
Работа с формами в Реакте — довольно трудоемкая задача. С одной стороны все крайне просто, с другой — появляется много однотипного кода. Поэтому для Реакта создано множество библиотек, позволяющих автоматизировать сохранение состояния формы. В основном, эти библиотеки нацелены на работу через redux . Подробнее — в соответствующем курсе.
Дополнительные материалы
JS: React → Неизменяемость
Неизменяемость состояния - одна из ключевых тем в Реакте. Ее легко придерживаться работая с примитивными типами данных, но с составными, такими как объекты и массивы, у неподготовленного пользователя могут возникнуть сложности. В этом уроке мы пробежимся по основным способам частичного обновления объектов и массивов.
Кроме примеров на чистом js, я буду демонстрировать примеры с использованием библиотеки immutability-helper
, которая создана для облегчения выполнения подобных операций. Она особенно актуальна при выполнении обновлений, там, где код на js получается слишком сложным.
Массивы
Массив: Добавление
Самое простое это добавление в массив:
const items = ['one', 'two', 'three'];
const item = 'four';
const newItems = [...items, item];
// => ['one', 'two', 'three', 'four'];
Если нужно добавить элемент в начало, то нужно всего лишь поменять местами элементы массива:
const newItems = [item, ...items];
// => ['four', 'one', 'two', 'three'];
immutability-helper
import update from 'immutability-helper';
const state1 = ['x'];
const state2 = update(state1, { $push: ['y'] }); // => ['x', 'y']
Массив: Удаление
Более интересный пример. Чтобы успешно выполнить удаление, нужно знать, что удалять. Это значит, что каждый элемент в коллекции должен иметь идентификатор. Для удаления используется старая добрая фильтрация.
const newItems = items.filter(item => item.id !== id);
Может возникнуть вопрос: откуда взялся идентификатор внутри обработчика? И здесь нам на помощь приходят замыкания.
https://codepen.io/hexlet/pen/JygbWP
Обратите внимание на способ задания обработчика: removeItem = (id) => (e) => {
и его использование onClick={this.removeItem(id)}
.
immutability-helper
const index = 5;
const newItems = update(items, {$splice: [[index, 1]]});
Удаление на чистом js через фильтр - самый оптимальный способ. С использованием immutabiliy-helper
получается сложно.
Изменение элемента массива
К сожалению, без дополнительных инструментов код решения будет слишком громоздким. Я приведу его ниже для ознакомления, но в реальном коде так делать не надо.
const index = items.findIndex((item) => item.id === id);
const newItem = { ...items[index], value: 'another value' };
const newItems = [...items.slice(0, index), newItem, ...items.slice(index + 1)];
Думаю, мне не придется вас убеждать в том, что это перебор
immutability-helper
const collection = { children: ['zero', 'one', 'two'] };
const index = 1;
const newCollection = update(collection, { children: { [index]: { $set: 1 } } });
// => { children: ['zero', 1, 'two'] }
Как видно, этот способ значительно проще и чище. Рекомендуется к использованию.
Объекты
Объект: Добавление
Так же просто, как и с массивом.
const items = { a: 1, b: 2 };
const newItems = { ...items, c: 3 };
// => { a: 1, b: 2, c: 3 }
Либо, если ключ вычисляется динамически, нужно делать так:
const items = { a: 1, b: 2 };
const key = 'c';
const newItems = { ...items, [key]: 3 };
// => { a: 1, b: 2, c: 3 }
Объект: Удаление
Решение ниже привожу только для ознакомления. На чистом js нет простого способа удалить ключ в неизменяемом стиле:
Object.keys(myObj)
.filter(key => key !== deleteKey)
.reduce((acc, current) => ({ ...acc, [current]: myObj[current] }), {});
immutability-helper
import update from 'immutability-helper';
const state = { a: 1, c: 3 };
const updatedState = update(state, {
$unset: ['c'],
});
// => { a: 1 }
Объект: Изменение
Абсолютно тоже самое, что и добавление.
const items = { a: 1, b: 2 };
const newItems = { ...items, a: 3 };
// => { a: 3, b: 2 }
immutability-helper
const data = { a: 1, b: 2 };
const key = 'a';
const newData = update(data, { [key]: { $set: 3 } });
// => { a: 3, b: 2 }
Глубокая вложенность
В примерах выше в основном можно обходиться стандартными средствами js, и только в некоторых ситуация удобнее пользоваться сторонними решениями. В реальном коде все будет также, особенно, если учитывать рекомендацию Реакта и держать свой стейт максимально плоским. Но в некоторых ситуациях данные, которые нужно изменить, находятся не на поверхности, а в глубине структур. К сожалению, в этих ситуациях, обычный js код будет раздуваться. И тут уже точно не обойтись без дополнительных библиотек.
Аналоги
immutability-helper
— не единственная библиотека для подобных задач. Вот еще несколько популярных:
-
immutable-js
- основана на персистентных данных -
updeep
- активно использует каррирование
JS: React → Вложенные компоненты
В реальных приложениях на Реакте компонентов значительно больше. Часть из них — самостоятельные, часть используется только в составе других.
Один из способов компоновки компонентов мы уже знаем - children
. Причем, нет никакой разницы, являются ли потомки встроенными в Реакт компонентами, или это написанные отдельно компоненты.
class Alert extends React.Component {
render() {
return (<div className="alert alert-primary">
{this.props.children}
</div>);
}
}
const vdom = <Alert>
<p>Paragraph 1</p>
<hr />
<p class="mb-0">Paragraph 2</p>
</Alert>;
ReactDOM.render(
vdom,
document.getElementById('react-root'),
);
В некоторых ситуациях внутрь компонента нужно передавать только определенные, специально созданные под него компоненты. Например компонент Card
до текущего момента мы реализовывали так, что он принимал на вход только свойства. В реальности это решение так себе. Кастомизация отсутствует полностью, можно передать только то, что изначально задумано и то в формате строк. Ни о каком сложном содержимом не может быть и речи. Правильный подход выглядел бы так:
<Card>
<CardImgTop src="path/to/image" />
<CardBody>
<CardTitle>Body</CardTitle>
</CardBody>
</Card>
В тех ситуациях, когда композиция не требуется, можно просто брать и использовать любые сторонние компоненты внутри своих.
https://codepen.io/hexlet/pen/XavMgZ
Вкладывать можно сколько угодно раз и какие угодно компоненты. Но здесь кроется одна опасность. Желание построить “идеальную архитектуру” толкает разработчиков заранее планировать то, как разбить приложение на компоненты и сразу их реализовать. Важно понимать, что вложенность сама по себе — это усложнение понимания, так как придется постоянно прыгать туда сюда. Кроме того, жесткая структура свяжет вас по рукам и ногам, рефакторинг просто так не сделаешь, и желание его делать сильно поубавится из-за любви к своему решению. Будьте прагматичны. Оптимальный путь добавлять новые компоненты — это следить за моментом, когда вам становится сложно в текущем компоненте из-за объемов и количества переменных, с которыми приходится иметь дело одномоментно. И даже в этом случае часто достаточно выделить дополнительные функции рендеринга внутри самого компонента, например так: renderItem
.
Состояние
Один из самых частых вопросов у тех, кто только начинает знакомиться с Реактом, связан с тем, как распределять состояние по компонентам. Короткий ответ: никак. Почти во всех ситуациях разделение состояния усложнит код и работу с ним. Правильный подход — создать корневой компонент, который содержит все состояние внутри себя, а все нижележащие компоненты получают свои данные как свойства. Само состояние должно быть максимально плоским, как реляционная база данных. Тогда можно спокойно применять нормализацию и безболезненно выполнять обновления.
Иногда могут возникать ситуации, что необходимые в глубине свойства приходится протаскивать сквозь множество промежуточных компонентов, которые сами эти свойства не используют. Это еще одна причина стараться не увлекаться глубокой вложенностью. С другой стороны, в следующем курсе мы познакомимся с Redux, который во многом решает эту проблему (и много других).
Колбеки
Из сказанного выше возникает еще одна сложность: что, если событие возникает в глубинном компоненте, у которого нет своего состояния? Без использования Redux выход, по сути, только один. Корневой компонент должен пробрасывать колбеки во внутренние компоненты, а те, в свою очередь, пробрасывают их дальше по необходимости.
https://codepen.io/hexlet/pen/XavRWy
JS: React → Функциональные компоненты
Для создания компонентов Реакта не обязательно использовать классы. В тех случаях, когда у компонента нет состояния, гораздо проще использовать альтернативный способ.
https://codepen.io/hexlet/pen/brXoER
Компоненты, созданные как функции, называются функциональными. Они принимают объект со свойствами как первый аргумент, и так же начинаются с большой буквы.
На вопрос “когда их стоит использовать?” ответ очень простой. Всегда, когда компонент не хранит в себе состояние. Другими словами, большинство компонентов в проекте должно быть именно функциональными.
В остальном они ведут себя точно так же, как и компоненты на классах.
Неймспейсы
Вспомним пример из прошлого урока, связанный с использованием компонентов-потомков, созданных специально для родительского компонента.
<Card>
<CardTitle>Title</CardTitle>
<CardBody>
<b>Body</b>
</CardBody>
</Card>
Следуя сказанному выше компоненты <CardTitle>
и <CardBody>
должны быть реализованы как функциональные.
Но это еще не все, можно пойти дальше и реализовать такую структуру:
import Card from './Card.jsx';
<Card>
<Card.Body>
<Card.Title>Title</Card.Title>
</Card.Body>
</Card>
JSX поддерживает механизм неймспейсов. Не сказать что без него нельзя жить, но он довольно удобен. Во-первых, достаточно импортировать только компонент верхнего уровня, а остальное доступно уже через него, что довольно логично если смотреть на JSX как на js код. Во-вторых, так лучше задается семантика.
Реализуется подобный механизм через статические свойства.
https://codepen.io/hexlet/pen/brXooa
Такой способ компоновки не требует того, чтобы все компоненты были созданы в одном файле. Структура может быть любой, для остального есть импорты.
JS: React → Virtual Dom
В предыдущем курсе мы впервые столкнулись c изменением DOM в процессе взаимодействия со страницей. Этот способ резко отличается от того, который мы использовали в курсе JS: DOM API. Важнейшее отличие связано с тем, как происходит изменение состояния отрисованного экрана. Напомню вкратце, что при прямом манипулировании DOM нам нужно сделать следующее:
- Удалить из DOM, то что стало неактуально для следующего состояния.
- Изменить, если надо, те элементы, которые присутствуют на экране и должны остаться в новом.
- Добавить новые элементы на страницу (точечно).
Другими словами, чтобы перейти в новое состояние, нужно изменить старое. Значит, про него надо знать.
В Реакте все совсем по-другому. После любого изменения и вызова setState
Реакт создает новое состояние и отрисовывает все компоненты, так, как будто это происходит с нуля. На самом деле отрисовка действительно происходит с нуля. Нам не важно, что было до этого момента на экране и как оно располагалось. Любое изменение в Реакте приводит к тому что приложение отрисовывается заново.
Создатели React называют этот подход, one-way data flow .
- Действия пользователя приводят к изменению состояния приложения (через
setState
). - Реакт запускает цикл отрисовки. Начиная от того компонента, в котором было изменено состояние (как правило, корневой компонент), через props данные постепенно распространяются от компонентов более высокого уровня до самых глубинных компонентов.
- Получившийся
html
интегрируется в страницу.
Те, кто хорошо знаком с функциональным подходом, могут увидеть прямую связь. Реакт действительно делает мир неизменяемым (immutable). Самый простой способ реализовать подобное поведение - использовать mountElement.innerHTML
. Который заменять целиком после вызова setState
. Хотя на практике этот подход сопряжен с кучей сложностей (я реализовывал подобную схему), он позволяет в 200 строк построить библиотеку, которая будет работать как React.
Главная проблема при использовании innerHTML
связана с производительностью. Сказать что это медленно — ничего не сказать. Поэтому создатели React пошли другим путем.
Virtual Dom Tree
Когда я говорил, что компоненты отрисовываются, то немного лукавил. В реальности после того, как отработает их рендеринг (вызов функции render
для всего дерева компонентов), создается так называемый виртуальный DOM. Это просто js-объект определенной структуры, который отражает состояние экрана. Далее React сравнивает новый virtual dom tree со старым и строит дифф (объект, описывающий разницу между старым и новым состоянием). И только в этот момент начинается отрисовка нового состояния в реальный DOM. Здесь уже должно быть понятно, что Реакт умнее, чем кажется на первый взгляд, и вносит изменения в реальный DOM настолько эффективно, насколько это возможно, ведь он знает КАК его надо изменить.
Из описанного выше есть важное следствие. Тот реальный DOM, который находится под контролем Реакта (это все потомки элемента, в который мы рендерим корневой компонент), не может изменяться никем снаружи Реакта. Если подобное произойдет, то Реакт не сможет нормально функционировать, ведь ему приходится трекать (отслеживать) текущее состояние DOM для того, чтобы производить вычисления диффа. Когда подобное происходит, Реакт ругается и говорит, что ему мешают работать.
Обращаю ваше внимание на то, что виртуальный DOM — не самоцель Реакта, как многие думают. Это просто эффективный способ реализовать идею one-way data flow . Если бы работал вариант с innerHTML
, то никто бы не делал виртуальный DOM.
И хотя построение js объекта — это гораздо более дешевая операция, чем работа с реальным DOM, все равно могут возникать ситуации, когда процесс вычисления занимает много времени, и это тормозит приложение. Об этом мы поговорим в одном из следующих уроков.
JS: React → Тестирование
Тестирование фронтенда — сложная задача, и создатели фреймворков всячески пытаются ее упростить. Реакт в этом плане, как мне кажется, продвинулся дальше всех, и не последнюю роль здесь сыграло то, что тестовый фреймворк jest
также разрабатывается Фейсбуком. Соответственно, уровень поддержки фронтенд тестирования и конкретно Реакта крайне высок.
JSDOM
jsdom - реализация DOM API на чистом js для использования в Node.js. Основной целью библиотеки является эмуляция подмножества функций браузера, достаточных для тестирования и парсинга сайтов. jsdom встроен в jest и не требует абсолютно никакой настройки. В этом легко убедиться, если открыть тесты Хекслета в любой практике, работающей с браузером. С точки зрения использования это выглядит так, что прямо в тесте у нас доступен document
и window
.
test('normalize', () => {
const expected = '<p class="row">Text</p>';
document.documentElement.innerHTML = expected;
normalize(document);
expect(document.body.innerHTML).toEqual(expected);
});
Возникает вопрос: зачем использовать jsdom , когда есть драйверы, работающие с настоящими браузерами. Ответов несколько:
- Скорость работы jsdom значительно выше, что не удивительно, ведь это просто библиотека на js (к тому же, headless), в отличие от браузера.
- jsdom потребляет значительно меньше памяти для работы.
- Самое главное: jsdom и код нашего приложения работают в рамках одного интерпретатора Node.js. На практике это приводит к тому, что любые ошибки внутри кода приложения будут проявляться так, как мы бы этого и хотели, с возникновением исключения и отображением стектрейса.
Единственный серьезный недостаток (он же и плюс) заключается в том, что jsdom — это не браузер. Другими словами, тесты на jsdom могут вполне работать, а код в браузере нет, и наоборот. Кроме того, jsdom сильно отстает в развитии от тех же браузеров. Новые фичи в нем появляются сильно позже, да и старые работают не все. Во многом эта проблема нивелируется использованием полифиллов, но если вы используете что-то уж совсем экзотическое, то, возможно, придется отказаться. По своей практике скажу, что с этим всем можно жить и полифиллы действительно спасают.
react-test-renderer
Так как Реакт генерирует виртуальный DOM, этим можно воспользоваться. Пакет react-test-renderer
предоставляет возможность отрендерить компонент Реакта без необходимости взаимодействия с браузером.
import reactTestRenderer from 'react-test-renderer';
const renderer = reactTestRenderer.create(
<a href="https://www.facebook.com/">Facebook</a>
);
console.log(renderer.toJSON());
// { type: 'a',
// props: { href: 'https://www.facebook.com/' },
// children: [ 'Facebook' ] }
С этим пакетом легко использовать снепшот-тестирование в jest. Достаточно передать в expect
результат вызова функции toJSON
.
Enzyme
Библиотека, разработанная программистами Airbnb для полноценного тестирования приложений на Реакте.
JS: React → Асинхронная обработка
Работа с асинхронным кодом в Реакт не отличается ничем особо примечательным по сравнению с тем, что мы уже проходили, но для проформы стоит пробежаться.
class Loader extends React.Component {
state = { url: null };
handleClick = async () => {
const res = await axios.get('/images/random');
this.setState({ url: res.data });
}
render() {
const url = this.state.url;
return (<div>
<button onClick={this.handleClick}>Load Random Image</button>
{url ? <img src={url} /> : null}
</div>
);
}
}
Выше видно, что мы легко можем делать обработчик асинхронным, а дальше все как обычно.
Единственный момент, выделяющий Реакт - это обработка событий в асинхронном коде. Как я уже упоминал, объект события в Реакте постоянно переиспользуется. Попытка работать с ним в асинхронном коде к хорошему не приведет:
onClick = (event) => {
console.log(event); // => nullified object.
console.log(event.type); // => "click"
const eventType = event.type; // => "click"
setTimeout(() => {
console.log(event.type); // => null
console.log(eventType); // => "click"
}, 0);
// Won't work. this.state.clickEvent will only contain null values.
this.setState({clickEvent: event});
// You can still export event properties.
this.setState({eventType: event.type});
}
Выходов из этой ситуации два: предпочтительный - взять из объекта события только то, что нужно и использовать; другой - вызывать event.persist()
, тогда Реакт не будет его больше трогать.
JS: React → Component Lifecycle
Обычный компонент в Реакте содержит основную функцию render
и выглядит так:
class Greeting extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
Или, что еще лучше, представляет из себя функцию. Но бывают ситуации, когда этого недостаточно. Типичная история — это инициализация данными. Например, после подключения компонента нужно сходить за данными на сервер и установить их в стейт. Выполнять эту задачу в функции render
по понятным причинам нельзя. Другой пример связан с задачей использовать внутри реакта библиотеки для него не предназначенные. В такой ситуации нужно иметь доступ к реальной DOM ноде.
Любой компонент Реакта сам по себе участвует в процессе своего существования, а раз есть процесс, то есть и конечный автомат. Этот процесс называют “Component Lifecycle”.
Как видно из картинки, компонент не просто рендерится, он так же вставляется в реальный DOM, удаляется из него, получает новые свойства, новое состояние и так далее. На каждый такой переход можно реализовать функцию-коллбек, которая будет вызвана Реактом во время соответствующего перехода.
Реакт проверяет, реализованы ли соответствующие функции в классе. Если да, то вызывает их в нужные моменты. Если нет, то ничего не происходит.
Ниже дана общая информация по функциям жизненного цикла компонентов, без глубокого погружения, так как все равно запомнить тысячи деталей их работы сложно. Более того — бессмысленно. Если есть общее понимание их работы, то несложно посмотреть в документацию и вспомнить, как оно работает.
Mounting
Процесс монтирования немного особый, так как выполняется ровно один раз.
-
constructor()
- обычный конструктор, мы уже имели с ним дело -
componentWillMount()
- вызывается перед тем, как монтировать компонент в DOM -
render()
- генерация виртуального DOM -
componentDidMount()
- вызывается после того, как компонент был вставлен в реальный DOM
Коллбеки монтирования отрабатывают только при появлении нового элемента в DOM. Это всегда справедливо для корневого компонента, который монтируется через ReactDOM.render()
. Остальные компоненты могут проходить этот процесс не раз. Все зависит от того, пропадают ли они из DOM или нет.
https://codepen.io/hexlet/pen/zEOVLr
В моей практике в основном используется componentDidMount
, так как в этот момент уже доступен DOM и можно с ним работать. Но это тема одного из следующих уроков.
Updating
componentWillReceiveProps()
shouldComponentUpdate()
componentWillUpdate()
render()
componentDidUpdate()
Коллбеки, описанные выше, запускаются только для обновления уже примонтированного компонента. Другими словами, они не запускаются тогда, когда компонент появляется в DOM в первый раз.
Unmounting
componentWillUnmount()
JS: React → Производительность
Преждевременная оптимизация - корень всех зол.
Перед тем, как рассуждать о производительности, я настоятельно рекомендую прочитать Optimization.guide
Для начала вспомним, что Virtual Dom — это уже оптимизация, которая позволяет Реакту из коробки работать достаточно быстро, чтобы мы могли вообще не задумываться о производительности долгое время. Многим проектам этого хватает за глаза на протяжении всей жизни.
Напомню, что Реакт работает так:
- Маунт вызывает рендеринг приложения.
- Получившийся DOM вставляется в реальный DOM целиком, так как там еще ничего нет. А виртуальный DOM, в свою очередь, сохраняется внутри Реакта для последующего обновления.
- Изменение состояния приводит к вычислению нового виртуального DOM.
- Вычисляется разница между старым виртуальным DOM и новым.
- Разница применяется к реальному DOM.
Reconciliation
Каждый раз, когда происходит изменение в состоянии компонента, запускается механизм, вычисляющий дифф между прошлым состоянием и новым. С алгоритмической точки зрения происходит поиск отличий в двух деревьях. В общем случае алгоритм, выполняющий это вычисление, работает со сложностью O(n3) .
Если события генерируются часто, а виртуальное дерево стало большим, то можно начать замечать лаги невооруженным глазом.
Для решения этой проблемы Реакт настоятельно просит для всех элементов списков использовать аттрибут key
, который не меняется для конкретного элемента списка. Подобное требование позволяет оптимизировать работу алгоритма уменьшив сложность до О(n) .
Требование проставлять ключи проверяется самим Реактом. Он сам будет выдавать варнинги (предупреждения) если увидит, что вы их не используете.
Rendering
На практике, рендеринг всего приложения (виртуального дома) на любое изменение - дорогое удовольствие. Представьте, что в приложении используется поле для текстового ввода. Это означает, что во время набора на любое нажатие происходит генерация виртуального дома целиком и с нуля. Хорошим примером является вопросы и ответы на Хекслете, где мы столкнулись именно с этой проблемой. В форуме достаточно большое виртуальное дерево, и его полный рендеринг занимает определенное время.
В ReactDevTools есть специальная галочка, нажав на которую можно увидеть те компоненты, которые рендерятся во время событий. Отображается все визуально, то есть после каждого события отрендеренные компоненты подсвечиваются рамочкой. Чем чаще они это делают, тем краснее рамка.
Легко заметить, что для приложения, в котором ничего специально не делалось, на любое событие будет рендерится вообще все. Но события, как правило, меняют только небольшую часть DOM. Ввод текста, часто, вообще не приводит к изменению в DOM.
Реакт позволяет избежать перерисовки тех компонентов, которые не изменились. Из условий — нужно соблюдать чистоту, другими словами, компонент должен, по сути, представлять из себя чистую функцию.
Напомню, что обновление компонентов запускает следующую цепочку функций:
componentWillReceiveProps(nextProps)
shouldComponentUpdate(nextProps, nextState)
componentWillUpdate(nextProps, nextState)
render()
componentDidUpdate(prevProps, prevState)
Остановить перерисовку можно благодаря наличию коллбек-функции shouldComponentUpdate()
. Если эта функция вернет false
, то компонент не будет рендерится вообще. А так как мы подразумеваем, что компонент ведет себя как чистая функция, то достаточно внутри этой функции проверить, что не изменился props
и state
. Выглядит это примерно так:
shouldComponentUpdate(nextProps, nextState) {
return !shallowEqual(this.props, nextProps)
|| !shallowEqual(this.state, nextState);
}
Shallow означает, что сравнивается только верхний уровень объектов. Иначе эта операция была бы слишком дорогой. Кстати, здесь становится видно, почему изменения состояния нельзя делать in-place: this.state.mydata.key = 'value'
. Так как объекты сравниваются по ссылкам, то изменение объекта будет показывать, что объект тот же самый, хотя его содержимое поменялось.
Поскольку большинство компонентов в типичных приложениях действительно ведут себя как чистые функции, а состояние хранится в общем корневом компоненте, подобную технику можно применять повсеместно, и Реакт нам в этом активно помогает. До сих пор в классах мы наследовались только от React.Component
, но можно наследоваться и от React.PureComponent
, в котором, за нас, правильно реализовали shouldComponentUpdate
.
https://codepen.io/hexlet/pen/MEWemR
Если нажимать кнопку, то видно, что корневой компонент перерендеривается, а вложенный нет.
Но не все так просто. Очень легко незаметно для самого себя сломать работу PureComponent
.
Default Props
Первая засада ожидает нас при неправильной работе со свойствами по умолчанию:
class Table extends React.Component {
render() {
return (
<div>
{this.props.items.map(i =>
<Cell data={i} options={this.props.options || []} />
)}
</div>
);
}
}
Казалось бы, безобидный код, но вызов []
каждый раз генерирует новый объект (при условии что options
falsy). Проверяется это легко: [] === []
ложно. То есть данные не поменялись, но <Cell>
будет отрисован заново.
Вывод: используйте встроенный механизм для свойств по умолчанию.
Callbacks
class App extends React.PureComponent {
render() {
return <MyInput
onChange={e => this.props.update(e.target.value)} />;
}
}
Проблема в коде выше точно такая же: на каждый вызов функции render
генерируется новая функция-обработчик, что ломает эффективное обновление. Выход мы уже знаем: определять обработчики как свойства на уровне класса.
Immutable.js
Еще один интересный способ решить проблему перерендеринга приложения - использовать персистентные структуры данных, а конкретно библиотеку immutable.js
. Это отдельная тема, рассмотрение которой находится за рамками текущего курса.
Дополнительные материалы
JS: React → Refs
Реакт по своей природе изолирует нас от прямой работы с DOM на 100%. Но нередко при интеграции сторонних не-Реакт компонентов возникает задача по прямому доступу к DOM. Также подобный механизм нужен для выделения текста, фокусов и проигрывания медиа.
Реакт позволяет сделать это с помощью ref
. Перед тем, как мы начнем его разбирать, хочу предупредить, что в нормальной ситуации он не нужен и следует максимально избегать его использования.
Рассмотрим задачу по фокусировке на поле ввода:
https://codepen.io/hexlet/pen/PJoGXM
ref
— это свойство компонента, значением которого должен быть объект, созданный в конструкторе через функцию React.createRef()
. Этот объект, в отличии от остальных данных, которые хранятся в props
или state
, хранится как обычное свойство объекта. Имя свойства можно выбрать произвольно. Свойство current
этого объекта дает доступ к элементу DOM, именно его можно использовать в componentDidMount
или componentDidUpdate
.
Ниже приведен пример создания компонента обертки над популярным JQuery плагином Chosen.
https://codepen.io/hexlet/pen/qPBjEB
ref
так же может использоваться и на самописных компонентах, реализованных как классы.
Функциональные компоненты не поддерживают аттрибут ref
, так как у них нет инстанса. Если вам нужна работа с DOM, то придется конвертировать такой компонент в класс.
Использование в реальном мире
С Реактом удобно и легко работать до тех пор, пока мы остаемся в рамках самого Реакта, но большая часть существующих js библиотек взаимодействует с домом напрямую, что фактически нивелирует преимущества реакта при их использовании напрямую. Например:
// https://github.com/kylefox/jquery-modal
$('#login-form').modal();
Включение в проект таких библиотек неизбежно приведет к активному использованию lifecycle методов и сделает код сложным. По этой причине, принято создавать так называемые врапперы, компоненты-обертки, которые скрывают внутри себя все взаимодействие в домом и наружу выставляют стандартный интерфейс реакта, а именно пропсы. Одной из таких задач является ресайз контейнера. Один из вариантов решения, компонент react-resizable. Посмотрите на работу этого компонента:
const Resizable = require('react-resizable').Resizable; // or,
const ResizableBox = require('react-resizable').ResizableBox;
// ES6
import { ResizableBox } from 'react-resizable';
// ...
render() {
return (
<ResizableBox width={200} height={200} minConstraints={[100, 100]} maxConstraints={[300, 300]}>
<span>Contents</span>
</ResizableBox>
);
}
Ничего в этом коде не напоминает о реальном доме. Все сводится к тому что мы оборачиваем наш компонент, в ResizableBox
, который скрывает всю работу внутри себя. По такому же принципу устроены сотни и может быть тысячи других компонентов, которые доступны на гитхабе. Вот некоторые из них: