Компоненты и коллекции
1. Веб-приложения
В современной веб-разработке изменились не только техники позволяющие веб-сайтам выглядеть лучше, загружаться быстрее и быть приятнее в использовании. В первую очередь изменились фундаментальные вещи - то, как мы проектируем и создаем веб-приложения.
Возьмем произвольный веб-сайт, например для работы с коллекцией рецептов, расписанием тренировок и т. п. Всегда есть набор страниц: домашняя, профиль, страница коллекции и страница одного элемента коллекции.
1.1. Multiple-page Application
Несколько лет назад мы бы использовали подход включающий несколько отдельных HTML-страниц.
Архитектура клиент-сервер
Вся логика живет на сервере
На каждый запрос сервер отсылает готовый HTML-документ
Перезагрузка страницы при каждом запросе
Плохая интерактивность
Отличное SEO
1.2. Single-page Application
Современный подход - сайт, на котором пользователь никогда не переходит на другие HTML-страницы. Интерфейс, вместо запроса HTML-документов с сервера, перерисовывается на клиенте, на одной и той же странице, без перезагрузки.
Архитектура клиент-сервер
При загрузке сайта сервер всегда отдает стартовую HTML-страницу
index.html
Каждый последующий запрос на сервер получает только данные в JSON-формате
Обновление интерфейса происходит динамически на клиенте
Загрузка первой страницы может быть довольно медленной (лечится)
Логика не связанная с безопасностью живет на клиенте
Слабое SEO (лечится)
Сложность кода и его поддержки масштабируется с кол-вом функционала приложения
Single-page application vs. multiple-page application
2. React
Библиотека для создания элементов интефрейса. В React нет встроенной маршрутизации, HTTP-модуля и т. п. Тем не менее есть богатая экосистема, которая позволит решить любую задачу.
При создании приложения с использованием React, разработчик не взаимодействует с DOM-деревом напрямую. Его задача описать интерфейс с помощью компонентов (шаблон) и управлять изменением данных (модель). React, при изменении данных модели, сам обновит интерфейс по шаблону.
React мультиплатформенный, разметку можно рендерить на сервере (Next.js), писать нативные (React Native) или десктопные (Electron) приложения.
3. Browser DOM и Virtual DOM
Browser DOM - древовидное представление HTML-документа, где каждый элемент документа представлен в виде DOM-узла. Хранится в браузере и напрямую связан с тем что мы видим на странице.
При каждом изменении DOM, браузер выполняет несколько трудоемких операций. Частые операции обновления такого дерева негативно влияют на производительность и отзывчивость интерфейса. Поэтому он медленный и обновлять его необходимо эффективно.
Virtual DOM - абстракция, легковесная копия реального DOM-дерева в виде JSON-документа.
Существует только в памяти и не рендерится в браузере
Не зависит от внутренней имплементации браузера
Использует лучшие практики обновления реального DOM
Собирает обновления в группы для оптимизации рендера (batching)
4. Инструменты
Для создания React-приложения необходимы Node.js
, Webpack
, Babel
, React
и DevTools
. Можно написать свою Webpack-сборку или взять любую хорошую с GitHub.
4.1. Create React App
Для обучения и маленьких/средних проектов рекомендуется использовать утилиту от авторов React.
Абстрагирует всю конфигурацию, позволяя сосредоточиться на написании кода
Включает необходимые инструменты:
Webpack
,Babel
,ESLint
и т. п.Расширяется дополнительными пакетами из экосистемы React
Имеет функцию извлечения, которая удаляет абстракцию и открывает конфигурацию
npx create-react-app имя_папки_проекта
npx — инструмент, предназначенный для того, чтобы помочь стандартизировать использование npm-пакетов. Поставляется с npm версии 5.2.0
и выше. npm
упрощает установку и управление зависимостями, размещенными в реестре, a npx
упрощает использование CLI-утилит и других исполняемых файлов без необходимости их установки в систему или проект.
4.2. React DevTools
В инструментах разработчика можно посмотреть на дерево компонентов, их состояние и пропсы. Профайлер полезен при оптимизации приложения.
5.React-элементы
React-элементы - это самые маленькие строительные блоки React, элементы Virtual DOM. Элементы это обычные JS-объекты, поэтому создавать их очень быстро.
Функция React.createElement()
это самый главный метод предоставляемый React API. Подобно document.createElement()
для DOM, React.createElement()
это функция для создания React-элементов. Возвращает объект, элемент Virtual DOM.
React.createElement(type, [props], [...children])
type
- имя встроенного React-элемента который в Virtual DOM соответсвует будущему HTML-тегу.props
- объект содержащий HTML-атрибуты и кастомные свойства. Может бытьnull
или пустой объект, если передавать ничего не нужно.children
- произвольное количество аргументов после второго это дети создаваемого элемента. Так создается дерево элементов.
import React from 'react';
const link = React.createElement(
'a',
{
href: 'https://reactjs.org/',
target: '_blank',
rel: 'noreferrer noopener',
},
'Ссылка на reactjs.org',
);
Создадим элемент с детьми, карточку продукта.
import React from 'react';
const image = React.createElement('img', {
src:
'https://images.pexels.com/photos/461198/pexels-photo-461198.jpeg?dpr=2&h=480&w=640',
alt: 'Tacos With Lime',
width: 640,
});
const title = React.createElement('h2', null, 'Tacos With Lime');
const price = React.createElement('p', null, 'Price: 10.99$');
const button = React.createElement('button', { type: 'button' }, 'Add to cart');
const product = React.createElement('div', null, image, title, price, button);
/*
* Для передачи детей также используется свойство children параметра props.
* Обратите внимание на то, что свойство children это массив.
*/
const productWithChildrenInProps = React.createElement('div', {
children: [image, title, price, button],
});
5.1. Рендер элемента в DOM-дерево
Для того чтобы отрендерить элемент, в пакете react-dom
есть метод ReactDOM.render()
.
Первым аргументом принимает ссылку на React-элемент или компонент (что рендерить)
Вторым, ссылку на уже существующий DOM-элемент (куда рендерить)
import ReactDOM from 'react-dom';
ReactDOM.render(product, document.getElementById('root'));
React использует модель отношений предок - потомок
, поэтому достаточно использовать только один вызов ReactDOM.render()
в приложении. Рендер самого верхнего элемента в иерархии повлечет за собой рендер всего поддерева.
6. JavaScript Syntax Extension (JSX)
Код в предыдущем разделе понятен браузеру. Но так описывать разметку интерфейса неудобно, нам привычен HTML. Для этого был создан JSX
.
Позволяет использовать XML-образный синтаксис прямо в JavaScript
Упрощает код, делает его декларативным и читабельным
Описывает объекты - элементы Virtual DOM
Это не HTML, Babel трансформирует JSX в вызовы
React.createElement()
В JSX можно использовать весь потенциал JavaScript
JSX не обязателен, но давайте сравним следующий код.
// Plain JavaScript
const link = React.createElement(
'a',
{
href: 'https://reactjs.org/',
target: '_blank',
rel: 'noreferrer noopener',
},
'Ссылка на reactjs.org',
);
// JSX
const linkWithJSX = (
<a href="https://reactjs.org/" target="_blank">
Ссылка на reactjs.org
</a>
);
JSX
значительно чище и читабельнее. Используя JSX
, компоненты становятся похожи на HTML-шаблоны. Перепишем карточку продукта.
import React from 'react';
import ReactDOM from 'react-dom';
const imageUrl =
'https://images.pexels.com/photos/461198/pexels-photo-461198.jpeg?dpr=2&h=480&w=640';
const price = 10.99;
const product = (
<div>
<img src={imageUrl} alt="Tacos With Lime" width="640" />
<h2>Tacos With Lime</h2>
<p>Price: {price}$</p>
<button type="button">Add to cart</button>
</div>
);
ReactDOM.render(product, document.getElementById('root'));
JSX преобразовывается в вызовы
React.createElement()
, поэтому пакет React должен быть в области видимости модуля.Можно использовать практически любое валидное JavaScript-выражение, оборачивая его в фигурные скобки.
Значения атрибутов указываются через двойные кавычки, если это обычная строка, и через фигурные скобки, если значение вычисляется, либо тип отличается от строки.
Все атрибуты React-элементов именуются в
camelCase
нотации.JSX-теги могут быть родителями других JSX-тегов. Если тег пустой или самозакрывающийся, его обязательно необходимо закрыть используя
/>
.
6.1. Правило общего родителя
Разберем следующий код с не валидной JSX-разметкой.
const post = (
<h2>Post Header</h2>
<p>Post text</p>
);
Перепишем код используя React.createElement()
.
const post = (
React.createElement('h2', null, 'Post Header')
React.createElement('p', null, 'Post text')
);
Невалидное выражение справа от оператора присваивания, потому что само по себе присваиваемое выражение не имеет смысла. Выражение это одно значение, результат неких вычислений, отсюда и правило общего родителя.
const post = React.createElement(
'div',
null,
React.createElement('h2', null, 'Post Header'),
React.createElement('p', null, 'Post text'),
);
В JSX это выглядит так.
const post = (
<div>
<h2>Post Header</h2>
<p>Post text</p>
</div>
);
Если в разметке лишний тег-обертка не нужен, используются Фрагменты
, похожие на DocumentFragment
. Этот встроенный компонент при рендере растворяется, подставляя свое содержимое.
import React, { Fragment } from 'react';
const post = (
<Fragment>
<h2>Post Header</h2>
<p>Post text</p>
</Fragment>
);
Синтаксис фрагментов можно сократить и не добавлять импорт Fragment
. Babel сделает все необходимые трансформации, заменив пустые JSX-теги на React.Fragment
.
import React from 'react';
const post = (
<>
<h2>Post Header</h2>
<p>Post text</p>
</>
);
6.2 Дополнительные материалы
7. Компоненты
Компоненты - основные строительные блоки React-приложений, при помощи которых интерфейс делится разделить на независимые части.
Разработчик создает небольшие компоненты, которые можно объединять, чтобы сформировать более крупные или использовать их как самостоятельные элементы интерфейса. Самое главное в этой концепции то, что и большие, и маленькие компоненты можно использовать повторно и в текущем и в новом проекте.
React-приложение можно представить как дерево компонентов. На верхнем уровне стоит корневой компонент, в котором вложено произвольное количество других компонентов. Каждый компонент должен вернуть JSX-разметку, тем самым указывая какой HTML мы хотим отрендерить в DOM.
7.1. Компоненты-функции
В простейшей форме компонент это JavaScript-функция с очень простым контрактом: функция получает объект свойств который называется props
и возвращает дерево React-элементов.
Имя компонента обязательно должно начинаться с заглавной буквы. Названия компонентов с маленькой буквы зарезервированы для HTML-элементов. Если вы попробуете назвать компонент card
, а не Card
, при рендере, React проигнорирует его и отрендерит тег <card></card>
.
const MyFunctionalComponent = props => <div>Functional Component</div>;
Компоненты-функции составляют большую часть React-приложения.
Меньше boilerplate-кода
Легче воспринимать
Легче тестировать
Нет контекста (this)
Сделаем карточку продукта компонентом-функцией.
const Product = props => (
<div>
<img
src="https://images.pexels.com/photos/461198/pexels-photo-461198.jpeg?dpr=2&h=480&w=640"
alt="Tacos With Lime"
width="640"
/>
<h2>Tacos With Lime</h2>
<p>Price: 10.99$</p>
<button type="button">Add to cart</button>
</div>
);
// В разметке компонент записывается как JSX-тег
ReactDOM.render(<Product />, document.getElementById('root'));
// Это аналогично
ReactDOM.render(React.createElement(Product), document.getElementById('root'));
8. Свойства компонента (props)
Свойства (пропсы) это одна из основных концепций React. Компоненты принимают произвольные свойства и возвращают React-элементы, описывающие что должно отрендерится в DOM.
Пропсы используются для передачи данных от родителя к ребенку.
Пропсы передаются только вниз по дереву от родительского компонента.
При изменении пропсов React ре-рендерит компонент и, возможно, обновляет DOM.
Пропсы доступны только для чтения, изменить их в ребенке нельзя.
Пропсом может быть текст кнопки, картинка, url, любые данные для компонента. Пропсы могут быть строками или результатом JS-выражения. Если передано только имя пропса - это буль, по умолчанию true
.
const App = () => (
<>
<h1>Best selling products</h1>
<Product name="Tacos With Lime" />
</>
);
Компонент <Product>
объявляет параметр props
, это всегда будет объект содержащий все переданные пропсы.
const Product = props => (
<div>
<h2>{props.name}</h2>
</div>
);
Добавим компоненту <Products>
несколько других свойств.
const Product = props => (
<div>
<img src={props.imgUrl} alt={props.name} width="640" />
<h2>{props.name}</h2>
<p>Price: {props.price}$</p>
<button type="button">Add to cart</button>
</div>
);
Сразу будем использовать простой паттерн при работе с props
. Так как props
это объект, мы можем деструктуризировать его в подписи функции. Это сделает код чище и читабельнее.
const Product = ({ imgUrl, name, price }) => (
<div>
<img src={imgUrl} alt={name} width="640" />
<h2>{name}</h2>
<p>Price: {price}$</p>
<button type="button">Add to cart</button>
</div>
);
const App = () => (
<div>
<h1>Best selling products</h1>
<Product
imgUrl="https://images.pexels.com/photos/461198/pexels-photo-461198.jpeg?dpr=2&h=480&w=640"
name="Tacos With Lime"
price={10.99}
/>
<Product
imgUrl="https://images.pexels.com/photos/70497/pexels-photo-70497.jpeg?dpr=2&h=480&w=640"
name="Fries and Burger"
price={14.29}
/>
</div>
);
В результате мы создали настраиваемый компонент который можно использовать для отображения товара. Мы передаем ему данные как пропсы, а в ответ получаем дерево React-элементов с подставленными значениями.
8.1. Свойство props.children
Концепция дочерних элементов позволяет очень просто делать композицию компонентов. В виде детей можно передавать компоненты, как встроенные так и кастомные. Это очень удобно при работе со сложными составными компонентами.
Свойство
children
автоматически доступно в каждом компоненте, его содержимым является то, что стоит между открывающим и закрывающим JSX-тегом.В функциональных компонентах обращаемся как
props.children
.Значением
props.children
может быть практически что угодно.
К примеру у нас есть компонент профиля <Profile>
и вспомогательный ui компонент <Panel>
, в который мы можем помещать произвольный контент.
const Profile = ({ name, email }) => (
<div>
<p>Name: {name}</p>
<p>Email: {email}</p>
</div>
);
const Panel = ({ title, children }) => (
<section>
<h2>{title}</h2>
{children}
</section>
);
const App = () => (
<div>
<Panel title="User profile">
<Profile name="Mango" email="mango@mail.com" />
</Panel>
</div>
);
В противном случае нам бы пришлось пробросить пропы для <Profile>
сквозь <Panel>
, что более тесно связывает компоненты и усложняет повторное использование.
8.2. Свойство defaultProps
Что если компонент ожидает какое-то значение, а его не передали? - при обращении к свойству объекта props
, получим undefined
.
Для того чтобы указать значения свойств по умолчанию, у компонентов есть статическое свойство defaultProps
, в котором можно указать объект с дефолтными значениями пропов (не обязательно всех). Этот объект будет слит с пришедшим объектом props
.
const Product = ({ imgUrl, name, price }) => (
<div>
<img src={imgUrl} alt={name} width="640" />
<h2>{name}</h2>
<p>Price: {price}$</p>
<button type="button">Add to cart</button>
</div>
);
Product.defaultProps = {
imgUrl:
'https://dummyimage.com/640x480/2a2a2a/ffffff&text=Product+image+placeholder',
};
/*
* Определение defaultProps гарантирует, что `props.imgUrl` будет иметь значение,
* даже если оно не было указано при вызове компонента в родителе.
*/
ReactDOM.render(
<Product name="Tacos With Lime" price={10.99} />,
document.getElementById('root'),
);
8.3. Свойство propTypes
Проверка типов получаемых пропсов позволит отловить много ошибок. Это экономит время на дебаг, помогает при невнимательности и спасает при росте приложения. В будущем будет необходимо выделить время и познакомиться с Flow или TypeScript, а для старта хватит небольшой библиотеки.
Пакет prop-types предоставляет ряд валидаторов для проверки корректности полученных типов данных во время исполнения кода, уведомляя о несоответствиях в консоли. Все что необходимо сделать это описать типы пропсов получаемых компонентом в специальном статическом свойстве propTypes
. Проверка пропов с помощью prop-types
происходит только во время разработки, в продакшене в ней нет необходимости.
npm install --save-dev prop-types
Используем prop-types
и опишем пропсы компонента Product
.
import PropTypes from 'prop-types';
const Product = ({ imgUrl, name, price }) => (
<div>
<img src={imgUrl} alt={name} width="640" />
<h2>{name}</h2>
<p>Price: {price}$</p>
<button type="button">Add to cart</button>
</div>
);
Product.defaultProps = {
imgUrl:
'https://dummyimage.com/640x480/2a2a2a/ffffff&text=Product+image+placeholder',
};
Product.propTypes = {
imgUrl: PropTypes.string,
name: PropTypes.string.isRequired,
price: PropTypes.number.isRequired,
};
Сначала применяются значения по умолчанию, заданные в defaultProps
. После запускается проверка типов с помощью propTypes
. Так что проверка типов распространяется и на значения по умолчанию.
Проверка типов с помощью PropTypes
9. Рендер по условию
Для рендера разметки по условию используются операторы ветвлений и условий. Условия можно проверять перед возвратом разметки, или прямо в JSX.
9.1. if с помощью логического оператора &&
Читается как: если условие приводится к true
, то рендерим разметку.
const Mailbox = ({ unreadMessages }) => (
<div>
<h1>Hello!</h1>
{unreadMessages.length > 0 && (
<p>You have {unreadMessages.length} unread messages.</p>
)}
</div>
);
9.2. if...else с помощью тернарного оператора
Читается как: если условие приводится к true
, рендерим разметку после ?
, в противном случае рендерим разметку после :
.
const Mailbox = ({ name, unreadMessages }) => (
<div>
<h1>Hello {name}.</h1>
{unreadMessages.length > 0 ? (
<p>You have {unreadMessages.length} unread messages.</p>
) : (
<p>No unread messages.</p>
)}
</div>
);
Последний пример можно записать по другому, результат будет одинаковый.
const Mailbox = ({ name, unreadMessages }) => (
<div>
<h1>Hello {name}.</h1>
<p>
{unreadMessages.length > 0
? `You have ${unreadMessages.length} unread messages.`
: 'No unread messages.'}
</p>
</div>
);
Пусть в компоненте продукта еще есть его доступное количество.
const Product = ({ imgUrl, name, price, quantity }) => (
<div>
<img src={imgUrl} alt={name} width="640" />
<h2>{name}</h2>
<p>Price: {price}$</p>
<h1>Quantity: {quantity < 20 ? 'Few left' : 'In stock'}</h1>
<button type="button">Add to cart</button>
</div>
);
9.3. Дополинтельные материалы
10. Коллекции
Для того чтобы отрендерить коллекцию однотипных элементов, используется метод Array.prototype.map()
, callback-функция которого, для каждого элемента коллекции, возвращает JSX-разметку. Таким образом получаем массив React-элементов который можно рендерить.
const favouriteBooks = [
{ id: 'id-1', name: 'JS for beginners' },
{ id: 'id-2', name: 'React basics' },
{ id: 'id-3', name: 'React Router overview' },
{ id: 'id-4', name: 'Redux in depth' },
];
const BookList = ({ books }) => (
<ul>
{books.map(book => (
<li>{book.name}</li>
))}
</ul>
);
ReactDOM.render(
<BookList books={favouriteBooks} />,
document.getElementById('root'),
);
10.1. Ключи
При выполнении кода из примера выше, всплывет предупреждение о том, что для элементов списка требуется ключ. React не может отличить элементы в коллекции, таким образом, перерисовывая всю коллекцию целиком при любых изменениях.
Ключ (key) — это специальный строковый проп, который нужно задать при создании элементов коллекции.
Элементы внутри коллекции должны быть обеспечены ключами, чтобы иметь стабильную идентичность. React использует ключи, чтобы определить, какие из элементов в коллекции необходимо создать и отрендерить заново, а не использовать элементы из предыдущего рендера. Так мы избегаем пересоздания всех элементов коллекции каждый раз, когда что-то меняется.
Ключи должны быть:
Уникальные - ключ элемента должен быть уникальным только среди его соседей. Нет смысла в глобально уникальных ключах.
Стабильные - ключ элемента не должен меняться со временем, изменением порядка элементов или после обновления страницы.
Лучший способ задать ключ — использовать статическую строку, которая однозначно идентифицирует элемент списка среди остальных. Чаще всего в качестве ключей используются идентификаторы объектов созданных базой данных - постоянное, неизменное значение.
const favouriteBooks = [
{ id: 'id-1', name: 'JS for beginners' },
{ id: 'id-2', name: 'React basics' },
{ id: 'id-3', name: 'React Router overview' },
{ id: 'id-4', name: 'Redux in depth' },
];
const BookList = ({ books }) => (
<ul>
{books.map(book => (
<li key={book.id}>{book.name}</li>
))}
</ul>
);
ReactDOM.render(
<BookList books={favouriteBooks} />,
document.getElementById('root'),
);
10.2. Дополнительные материалы
Last updated
Was this helpful?