- Что такое React и зачем он нужен
- Чем отличаются фреймворк и библиотека
- Синтаксическое расширение JSX
- Элементы и компоненты
- Функциональные и классовые компоненты
- Параметры компонента
props
- Состояние компонента
state
- Однонаправленный поток данных (
one-way data flow
) - Жизненный цикл компонента (
component lifecycle
) - React Hooks
- Контролируемые и неконтролируемые компоненты
- Компоненты высшего порядка
- Работа React под капотом
Определение ниже как нельзя точно описывает, что такое React.
React is a free and open-source front-end JavaScript library for building user interfaces based on UI components. Разберём его.
Итак, начнём.
Во-первых, React - это JavaScript-библиотека.
Для полноценной работы с React достаточно просто подключить скрипт библиотеки к документу страницы (например, добавив его index.html
).
Иногда React называют JavaScript-фреймворком и ставят его в сравнение с такими фреймворками как Angular и View. Действительно, эта троица стоит сравнения, но всё же называть React фреймворком не стоит.
Во-вторых, React код библиотеки находится в исходном доступе, а её использование на вашем проекте является абсолютно бесплатным.
В-третьих, библиотека React нацелена на облегчения написания интерактивных UI-компонент на языке JavaScript.
Таким образом, при использовании React склоняются к компонентному подходу (англ. component-based approach
). Это значит, что сайт состоит из набора компонент, которые могут переиспользоваться в любом месте сайта.
Примерами UI-компонент являются кнопки, формы, поля для ввода, меню, блоки с информацией - проще говоря, любой DOM-элемент или группу элементов можно обернуть в компонент, чтобы его можно было переиспользовать.
JavaScript XML, JSX - это синтаксическое расширение языка JavaScript, которое позволяет писать HTML в одном файле с JavaScript.
Допустим, мы имеем следующий HTML-документ нашего сайта.
<!-- public/index.html -->
<!DOCTYPE html>
<html lang="en">
<head><!-- метаданные --></head>
<body>
<div id="root">
<!-- пустой блок, в который React в последствии будет вставлять элементы, составляющие приложение -->
</div>
</body>
</html>
Рассмотрим простейшее React-приложение, состоящее всего из одного файла с использованием синтаксиса JSX
(расширение .jsx
):
/* App.jsx - корневой файл React-приложения */
import React from 'react';
import ReactDOM from 'react-dom';
const headerElement = (
<div className="header">
<h1 className="author">Max-Starling</h1>
<h2 className="repository">Notes</h2>
<h3 className="topic">React JSX</h3>
</div>
);
const rootElement = document.getElementById('root');
ReactDOM.render(headerElement, rootElement);
Без JSX
код приложения бы выглядел следующим образом:
/* App.js - корневой файл React-приложения */
import React from 'react';
import ReactDOM from 'react-dom';
const headerElement = React.createElement(
"div",
{ className: "header" },
React.createElement("h1", { className: "author" }, "Max-Starling"),
React.createElement("h2", { className: "repository" }, "Notes"),
React.createElement("h3", { className: "topic" }, "React JSX")
);
const rootElement = document.getElementById('root');
ReactDOM.render(headerElement, rootElement);
Мы сможем ещё раз убедиться в таком результате, когда будем вести речь о Babel
.
Попробуйте теперь на основании этого примера представить приложение побольше. Становится понятно, что в больших приложениях, при большой вложенности элементов, HTML-подобный синтаксис читается проще, чем JS-подобный. Это понятно разработчику, но не понятно браузеру, поэтому в следующей главе затронем тему совместимости JSX и браузера.
Синтаксические расширения могут значительно облегчить жизнь любому разработчику, однако браузеры способны распознавать лишь чистый синтаксис HTML5, CSS3 и JavaScript - они не понимают JSX, TypeScript, SCSS, Handlebars и другие синтаксические расширения.
Например,
JSX
позволяет вам хранить HTML и JS в одном месте,TypeScript
наделяет JavaScript статической типизацией (то есть переменныым можно явно задавать типы данных, изменение которых по ходу выполнения программы приводит к ошибке),SCSS
позволяет использовать переменные, вложенные селекторы и циклы вCSS
,Handlebars
позволяет использовать переменные вHTML
.
Для того, чтобы программист мог использовать подобные возможности, нужны специальные программы, которые смогут понять нужный синтаксис, обработать его и перевести его на понятный браузеру манер (а именно в HTML5
, CSS3
и JavaScript
, соответствующий поддерживаемой браузером спецификации EcmaScript
).
Такие программы называют по-разному: транспайлеры, компиляторы, препроцессоры, шаблонизаторы и так далее, но результатом их выполнения всегда будет понятный браузеру синтаксис либо ошибка (чаще всего синтаксическая ошибка приводит к ошибке компиляции).
Слева то, что видит пользователь, справа то, что видит браузер, а значит и пользователь.
Проверить это вы можете сами здесь.
- Элемент
- Компонент
- Различие между элементом и компонентом
- Вызов компонента как функции
- Детальнее о React.createElement
React-элемент (англ. element
) имеет такое же предназначение, как и HTML-элемент: он является строительным блоком, при помощи которого задаётся разметка страницы. Но, в отличии от HTML-элемета, React-элемент является обыкновенным объектом.
При этом JSX позволяет использовать синтаксис, похожий на HTML, в JavaScript, что делает React-элементы визуально неотличимыми от HTML-элементов.
const element = (<span>Notes</span>);
На самом же деле, подобный синтаксис является синтаксическим сахаром, который после обработки транспайлером Babel преобразуется в обычный вызов JavaScript-функции:
const element = React.createElement('span', null, 'Notes');
Для того, чтобы понял, как React-элемент преобразуется в HTML-элемент, читайте далее про Virtual DOM.
Если элемент является константой (каким-то заданным, неизменяемым блоком), то компонент (англ. component
) является функцией (классом), которая может создавать элементы в зависимости от переданных ей параметров.
Единственным входным параметром компонента является объект "props" (англ properties
- свойства).
/* функциональный компонент */
const Title = props => (<span>{props.text}</span>);
/* классовый компонент */
class Title extends React.Component {
render() {
return (
<span>{this.props.text}</span>
);
}
}
Синтаксис JSX позволяет использовать компоненты так же, как и React-элементы:
<Title text="Notes" />
Такой синтаксис эквивалентен вызову функции Title
, которая возвращает React-элемент, или созданию экземпляра класса Title
, который возвращает React-элемент в своём методе render()
.
При этом атрибут text
(как и другие атрибуты, если они есть) вместе со своим значеним "Notes"
попадает в props
функциональной компоненты Title
, то есть props = { text: 'Notes' }
, или в this.props
классовой компоненты Title
, то есть this.props = { text: 'Notes' }
.
Таким образом, строка
<Title text="Notes" />
неявно преобразуется в
React.createElement(Title, { text: 'Notes' }, null);
React-элемент выступает в роли чего-то статического, неизменного, то есть некоторой константы, экземпляра класса - проще говоря, обычного объекта, а его приближённым аналогом является DOM-элемент.
React-компонент выступает в роли конструктора элементов, то есть в роли функции, строителя (англ. builder
), класса, но при этом у компонента имеются жизненный цикл, состояние, поведение, поэтому он всегда ассоциируется у меня с чем-то динамическим, меняющимся (реагирующим) в ответ на некоторые внешние и внутренние фатороы (здесь имеются в виду параметры компонента props
и внутреннее состояние state
, с которыми мы познакомимся позже).
На каком-то безумном уровне абстракции вы можете сравнить элементы с чем-то неодушевлённым (скажем, с камнем), а компоненты с одушевлённым (живущим своей полноценной жизнью организмом).
Вызов компонента как функции был запрещён в одной из версий React.
Title({ title }); // Error
Такое ограничение можно снять, если создать фабрику с помощью метода React.createFactory
.
const Title = React.createFactory(({ text }) => (<span>{text}</span>));
Данный подход может быть полезен для тех, кто по каким-то причинам не может использовать JSX.
Метод React.createElement(element, props, ...children)
принимает три параметра.
Если используется React-элемент, то
element
— это тег элемента, переданный в виде строки. Например,'div'
.props
— это атрибуты React-элемента. В большинстве случаев совпадают с атрибутами HTML-элемента, но есть исключения: HTML-атрибутclass
заменён наclassName
, атрибуты по типуz-index
- наzIndex
, а атрибуты по типуonclick
- наonClick
.children
— список дочерних элементов текущего элемента (все параметры, начиная с третьего, относятся к списку дочерних компонент).
Если используется React-компонент, то
element
— название компоненты (функции или класса). Например,Title
.props
— атрибуты React-элемента (в большинстве случаев совпадают с атрибутами HTML-элемента) или объектprops
компонента.children
— список дочерних элементов компонента.
Итак, JSX ниже
const Header = (props) => { /* .... */ };
const Menu = (props) => { /* .... */ };
const logout = () => { /* .... */ };
const headerElement = (
<Header>
<Menu items={['home', 'user']} />
<button onClick={logout} />
</Header>
);
преобразуется в
const Header = (props) => { /* .... */ };
const Menu = (props) => { /* .... */ };
const logout = () => { /* .... */ };
const headerElement = React.createElement(
Header,
{},
React.createElement(Menu, { items: ['home', 'user'] }, null),
React.createElement('button', { onClick: logout }, null),
);
- О жизненном цикле компонента
- Жизненный цикл классового компонента
- Монтирование классового компонента
- Обновление классового компонента
- Размонтирование классового компонента
Если говорить простыми словами, то жизненный цикл компонента не сильно отличается от жизненного цикла любого живого существа.
Живое существо:
- Рождается,
- Живёт и изменяется с течением времени (то есть изменяет своё состояние в следствие каких-либо внутренних или внешних причин),
- Умирает.
Компонент:
- Монтируется, то есть инициализируется, создаётся экземпляр компонента (в памяти компьютера) и компонент отрисовывается первый раз. Конечным результатом является то, что реальный пользователь браузера может видеть результат отрисовки компонента у себя на экране.
- Обновляется, то есть перерисовывается при изменениях во внутреннем состояннии
state
и внешнем состоянииprops
. - Размонтируется, то есть удаляется (из памяти компьютера сборщиком мусора).
Рассмотрим жизненный цикл классового компонента и методы его жизненного цикла (англ. lifecycle methods
, lifecycle hooks
).
- Метод
constructor()
отвечает за инициализацию компонента. static getDerivedStateFromProps()
- статический метод, позволяющщийrender()
componentDidMount()
static getDerivedStateFromProps()
shouldComponentUpdate()
render()
getSnapshotBeforeUpdate()
componentDidUpdate()
и уже устаревшие:UNSAFE_componentWillUpdate()
UNSAFE_componentWillReceiveProps()
componentWillUnmount()
Компонент создаётся, изменяется и удаляется из памяти, когда в нём нет больше не обходимости.
Любое изменение компонента можно отловить при помощи методов жизненного цикла (англ. lifecycle methods
).
Раньше жизненный цикл компонента можно было отслеживать лишь в классавых компонентах, но с появленем React Hooks такая возможность появилась и для функциональных компонент.
Различия во времени выполнения:
- Хук
useEffect
выполняется асинхронно (англ.asynchronously
) после рендера компонента и после отрисовки (англ.painting
), то есть хук не блокирует процесс отрисовки. - Хук
useLayoutEffect
выполняется синхронно (англ.synchronously
) после всех DOM-мутаций, но перед отрисовкой, то есть как только DOM готов, но ещё не отрисован. Это позволяет произвести вычисления или действия с DOM, которые повлияют на последующую отрисовку. Оба хука. - Таким образом,
useLayoutEffect
отрабатывает всегда раньше, чемuseEffect
.
Различия в производительности:
- Хук
useEffect
выполняется асинхронно, а значит не блокирует отрисовку, на фоне и не создаёт задержек отрисовки, не влияет на производительность. - Хук
useLayoutEffect
выполняется синхронно, а значит отрисовка ожидает его выполнения. Тяжелые вычисления или дополнительные обновления DOM в этом хуке затормаживают отрисовку, влияя на производительность сайта в целом.
Когда использовать:
- Хук
useEffect
используется в 99% случаев для большинства асинхронных задач (запросы к серверу (async..await
,promise
),dispath
событий по какому-то условию, таймерыsetTimeout
, подписка на события (англ.subscribtion
) и так далее. - Хук
useLayoutEffect
следует использовать лишь в крайних случаях, когда как можно быстрее нужно вычислить размеры элементов DOM или перехватить и изменить DOM напрямую перед отрисовкой (применить стили, переместить элементы и так далее).
Компонент высшего порядка memo
служит заменой метода shouldComponentUpdate
и PureComponent
для функциональных компонент.
const Article = ({ title }) => (<div>{title}</div>);
const areEqual = (props, nextProps) => {
if (props.title === nextProps.title) {
return true; // компонент не будет перерендерен
}
return false; // компонент будет перерендерен
};
const React.memo(Article, areEqual);
Все действия в функциональном компоненте (не считая React Hooks) проделываются каждый раз, когда он рендерится. В том числе и создание переменных.
const Form = ({ title }) => {
/* функция onSubmit пересоздаётся в компоненте Form на каждый рендер */
const onSubmit = () => console.log('submit', { title });
/* console.log запускается каждый раз, когда рендерится компонента */
console.log('rerender!');
return (<button onClick={onSubmit}>{title}</button>);
};
Можно столкнуться со следующей проблемой.
Пусть есть список из 100 элементов, состояние которых меняется по клику.
import React, { memo, useState, useCallback } from "react";
import ReactDOM from "react-dom";
const ListItem = ({ value, index, onClick } => {
console.log("rendered!");
const onButtonClick = () => onClick(index, 1);
return <button onClick={onButtonClick}>{value}</button>;
};
const List = () => {
const [items, setItems] = useState(Array.from(Array(100).fill(0)));
const onClick = (index, value) =>
setItems(items => items.map((item, i) => (i === index ? value : item))),
);
const renderItem = (value, index) => (
<ListItem key={index} index={index} value={value} onClick={onClick} />
);
return <div>{items.map(renderItem)}</div>;
};
ReactDOM.render(<List />, document.getElementById("root"));
При клике на любой элемент списка в консоли можно увидеть (100) rendered!
. Это означает, что клик по одному элементу заставить перерендериться все остальные. Попробуем это исправить.
Обернём компонент ListItem
в memo
, чтобы он перерендеривался не каждый раз при рендере родительского компонента, а только при обновлении его props
.
const ListItem = memo({ value, index, onClick }) => {
console.log("rendered!");
const onButtonClick = () => onClick(index, 1);
return <button onClick={onButtonClick}>{value}</button>;
});
При клике на любой элемент списка всё ещё можно видеть (100) rendered!
. Это связано с тем, что функция onClick
пересоздаётся каждый раз и в Props
каждому элементу приходит её новая версия. Пересоздания функции можно избежать, если мемоизировать функцию при помощи useCallback
.
const onClick = useCallback(
(index, value) =>
setItems(items => items.map((item, i) => (i === index ? value : item))),
[]
);
useCallback(callback, [dependencies])
.
Контролируемый компонент (Controlled component) контролирует данные форм при помощи возможностей React. Происходит двухстороннее связывание при помощи value
и onChange
с полями ввода, а данные этих полей должны где-то сохранятся (например, в React State или Redux Store).
const Form = ({ onSend, value, onChange }) => (
<form onSubmit={onSend}>
<input
type="text"
value={value}
onChange={onChange}
/>
<button type="submit">Send</button>
</form>
);
Неконтролируемый компонент (Uncontrolled component) контролирует данные форм при помощи DOM. Данные полей ввода сохраняются в атрибутах их DOM-элементов.
const Form = ({ onSend }) => (
<form onSubmit={onSend}>
<input type="text" />
<button type="submit">Send</button>
</form>
);
Можно задать значение по умолчанию для некотролируемого компонента при помощи свойства defaultValue
и получить текущее значение, используя ref
.
const Form = ({ onSend }) => {
const inputRef = React.useRef(null);
const onSubmit = (event) => {
event.preventDefault();
console.log('value', inputRef.current.value);
onSend(event);
}
return(
<form onSubmit={onSubmit}>
<input
ref={inputRef}
defaultValue="Text"
type="text"
/>
<button type="submit">Send</button>
</form>
);
};
Неконтролируемые компоненты могут использоваться
- когда нужно написать код быстро (например, что-то проверить).
- когда нет необходимости контролировать промежуточное состояние полей ввода, важен только конечный результат при нажатии на
submit
. - когда нужно интегрировать в приложение не React-код (HTML + JavaScript, Web Components или что-то ещё).
- всегда в случае
<input type="file" />
, поскольку его значение нельзя контролировать программно.
Во всех остальных случаях не рекомендуется использовать неконтролируемые компоненты, поскольку они хранят свои данные в DOM и React перестаёт быть единственным местом хранения данных.
Минимальная реализация компонента высшего порядка withRouter
.
const router = { route: 'qq' };
const withRouter = Component => props => (
<Component {...props} router={router} />
);
const Navbar = withRouter(props => <div>Route: {props.router.route}</div>));
const render = () => (<Navbar />); // <div> Route: qq </div>
Минимальная реализация компонента высшего порядка connect
.
const state = { username: 'Max' };
const dispatch = action => console.log(action);
const connect = (mapStateToProps, mapDispatchToProps) => Component => props => (
<Component
{...props}
{...mapStateToProps(state, props)}
{...mapDispatchToProps(dispatch)}
/>
));
const ProfileComponent = props => (
<div>
<p>{props.username}</p>
<button onClick={props.changeUsername}>Change name</button>
</div>
);
const mapStateToProps = state => state.username;
const mapDispatchToProps = dispatch => ({
changeUsername: name => dispatch({ type: 'SET_USERNAME', payload: name }),
});
const Profile = connect(ProfileComponent)(mapStateToProps, mapDispatchToProps);
const render = () => (<Profile >/);
Как мы знаем, в браузере есть представление HTML в виде дерева, которое называется объектной моделью документа (англ. Document Object Model, DOM
).
Взаимодействие с DOM происходит медленно. Операции над DOM являются достаточно трудоёмкими.
Поэтому разработчики React создали свою собственную документную модель - абстракцию над реальным DOM браузера, которая называется виртуальной объектной моделью документа (англ. Virtual DOM
).
Таким образом, DOM
- это абстракция над HTML
, а Virtual DOM
- это абстракция над DOM
.
Виртуальный DOM позволяет ослеживать изменения React-элементов и затем вносить в реальный DOM (в html) только те изменения, которые на самом деле произошли.
React самостоятельно следит за обновлениями.
Согласование, сверка (Reconciliation) — алгоритм сравнения (diffing algorithm) двух деревьев, используемый в React для определения различий между ними.
Сравнение двух деревьев начинается с их корневых элементов (root elements), от типа которых зависит дальнейшее поведение.
Если тип корневых элементов различен, React уничтожает (tear down) старое дерево и строит новое с нуля.
При уничтожении дерева старые DOM-узлы (DOM nodes) удаляются. Экземпляры компонента (component instances) получают componentWillUnmount()
. При построении нового дерева новые DOM-узлы добавляются в DOM. Экземпляры компонента получают componentWillMount()
и затем componentDidMount()
. Любое состояние, связанное со старым деревом, теряется.
Пример: <div>
, <p>
, <Title>
, <input>
, <Description>
— все перечисленные далее элементы имеют различные типы и их замена друг на друга приведёт к уничтожению дерева, то есть корневого элемента вместе с его детьми.
<!-- /* замена <div> */ -->
<div>
<Title>Notes</Title>
<Description>My simple notes abount everything.</Description>
</div>
<!-- /* на <article> */ -->
<article>
<Title>Notes</Title>
<Description>My simple notes abount everything.</Description>
</article>
<!-- /* приведёт к уничтожению (unmount) старых <Text>, <Description>
и созданию (remount) новых /* -->
Если корневые элементы имеют одинаковый тип, React сравнивает их атрибуты и обновляет только изменённые атрибуты (в случае атрибута style
— только изменённые стили).
<!-- /* при замене */ -->
<button
className="btn"
style={{ border: 'none', background: 'black' }}
onClick={onClick}
tabIndex={1}
/>
<!-- /* на */ -->
<button
className="button"
style={{ border: 'none', background: 'grey' }}
onClick={onClick}
tabIndex={1}
/>
<!-- /* React обновит только className и background в style */ -->
После обработки корневого элемента React рекурсивно проходится по дочерним элементам, являющимися корневыми в поддеревьях.
Если корневые элементы являются компонентами одного типа, React оставляет тот же экземпляр компонента (его состояние не теряется между двумя render()
), обновляет Props экземпляра и вызывает в нём componentWillReceiveProps()
и componentWillUpdate()
. Далее вызывается render()
и начинается рекурсивный алгоритм сравнения результатов предыдущего и нового render()
.
<!-- /* при замене */ -->
<Article>Text</Article>
<!-- /* на */ -->
<Article>Notes</Article>
<!-- /* тип компонента остался без изменений: изменился только props.children;
значит экземпляр остаётся прежним и обновляются его Props */ -->
При рекурсивном обходе дочерних элементов React проходит по двум спискам потомков (старое и новое дерево) корневого элемента одновременно и создаёт мутацию, если находит отличие.
Таким образом, если удалить первый элемент списка, то порядок сместится и все элементы будут считаться различными.
Пусть есть список, отображающий элементы массива items
, состоящего чисел от 0 до 99.
const items = Array.from(Array(100).keys()); // [0, 1, ..., 99]
const renderItem = item => (<li>{item}</li>);
<ul>{items.map(renderItem)}</ul>;
После удаления первого элемента (например, при помощи items.shift()
) массива, элемент 0 в старом дереве будет сравниваться с элементом 1 в новом, 1 с 2, 2 с 3 и так далее. Все элементы окажутся различными и будут обновлены.
Этого можно избежать при помощи ключей, устанавливающихся в атрибут key
.
const renderItem = item => (<li key={item}>{item}</li>);
При наличии ключей React
пытается сопоставить элементы по этим ключам. Если элементы по ключам совпадают, то они остаются без изменений.
Для правильной работы этого алгоритма важно, чтобы ключи были уникальными. Поэтому в ключ нужно передавать уникальное значение, присущее элементу (например, id
). Если уникальные значения отсутствуют, лучше всего их разово сгенерировать и использовать (например, при помощи uuid()
. В примере выше уникальным значением является сам item
(число).
index
элемента в массиве использовать не рекомендуется. При его использовании в примере выше произойдёт аналогичная ситуация (под индексом 0 в старом массиве лежит 0, а в новом 1). Использовать индекс можно только в том случае, когда порядок элементов не меняется (например, когда элементы могут добавляться только в конец и оттуда же).
Ключи должны быть стабильными, предсказуемыми и уникальными. Нестабильные ключи (например, key={Math.random()}
) вызовут необязательное пересоздание экземпляров компонента и DOM-узлов, что приводит к потере состояния дочерних компонентов и падению производительности.