Основные концепции React.js, о которых стоит знать
В этой статье рассмотрим основные концепции React.js – компоненты, жизненный цикл и обработка событий.
Компоненты – основа всего
Основополагающая концепция React.js – многоразовые компоненты. Разработчик создает небольшие части кода, которые можно объединять, чтобы сформировать более крупные или использовать их как самостоятельные элементы интерфейса.
Самое главное в этой концепции то, что и большие, и маленькие компоненты можно использовать повторно и в текущем и в новом проекте.
В простейшей форме компонент Реакта – это javascript-функция.
function Button (props) { // Возвращает DOM элемент. Например: return <button type="submit">{props.label}</button>; } // Отрисовываем компонент Button в браузере ReactDOM.render(<Button label="Save" />, mountNode)
Зачем здесь фигурные скобки – рассмотрим чуть позже, пока о них не стоит беспокоиться. Что такое ReactDOM тоже разберем позднее.
Второй аргумент в ReactDOM.render – это элемент документа, с которым будет работать Реакт.
На что следует обратить внимание в этом примере:
- Названия компонентов начинаются с заглавной буквы. Это важно, так как в работе будут сочетаться HTML-элементы и элементы Реакта. Названия со строчных букв зарезервированы для HTML. Если вы попробуете назвать элемент просто button, при рендере фреймворк проигнорирует его и отрисует обычную HTML-кнопку.
- Каждый элемент имеет список свойств (атрибутов), как и в HTML. В Реакте это называется props.
- Функция render принимает так называемый JSX – это HTML, помещенный в JavaScript и разбавленный специальным синтаксисом.
JSX
Первый пример может быть написан на чистом синтаксисе React.js, без JSX:
function Button (props) { return React.createElement( "button", { type: "submit" }, props.label ); } // Чтобы использовать Button вы должны написать что-то наподобие // этого: ReactDOM.render( React.createElement(Button, { label: "Save" }), mountNode );
В Api Реакта функция createElement является основной функцией верхнего уровня. Подобно document.createElement для DOM, эта функция нужна для создания компонента в ReactDOM. В отличие от первой, Реакт-функция принимает неограниченное число аргументов после второго аргумента. Таким образом с createElement, фактически, создается дерево.
const InputForm = React.createElement( "form", { target: "_blank", action: "https://google.com/search" }, React.createElement("div", null, "Enter input and click Search"), React.createElement("input", { className: "big-input" }), React.createElement(Button, { label: "Search" }) ); // InputForm использует компонент Button, поэтому мы должны определить его: function Button (props) { return React.createElement( "button", { type: "submit" }, props.label ); } // После этого мы можем использовать InputForm для передачи в функцию render ReactDOM.render(InputForm, mountNode);
На что следует обратить внимание в этом примере:
- InputForm не является компонентом React.
- После первых двух аргументов было передано еще несколько. Полный список аргументов, начиная с третьего содержит список дочерних элементов создаваемого родительского.
- Второй аргумент может быть пустым объектом или null, если элементу не требуются атрибуты и свойства.
HTML-элементы и компоненты Реакта можно смешивать. Код в примерах выше – это то, что браузер начнет понимать, если подключить библиотеку React.js. Напрямую браузер не будет работать с JSX. Однако, создавать сайт используя только createElement – было бы очень неудобно, поэтому существует JSX, с которым можно работать как с привычной HTML-разметкой.
const InputForm = <form target="_blank" action="https://google.com/search"> <div>Enter input and click Search</div> <input className="big-input" name="q" /> <Button label="Search" /> </form>; // InputForm все еще использует компонент Button, значит мы должны его определить. // Это можно сделать либо с помощью JSX либо используя createElement function Button (props) { // Возвращает DOM элемент. Например: return <button type="submit">{props.label}</button>; } // После этого мы можем использовать InputForm для передачи в функцию render ReactDOM.render(InputForm, mountNode);
Тем не менее, в браузере будет использоваться скомпилированная версия этого кода (как в предыдущем примере). Чтобы этот код заработал, требуется использовать препроцессор для преобразования JSX в понятный браузеру javascript-код с React.createElement.
JavaScript-выражения можно использовать в любом месте JSX
Внутри JSX можно использовать любое js-выражение, заключив его в фигурные скобки.
const RandomValue = () => <div> { Math.floor(Math.random() * 100) } </div>; // Отрисовываем компонент: ReactDOM.render(<RandomValue />, mountNode);
При работе с JSX есть ограничение: внутри разметки можно использовать только готовые выражения. Нельзя, например, использовать конструкцию if/else, но можно заменить ее тернарным оператором.
Переменные также подходят под это определение, поэтому, когда компонент получает список свойств, возможно использовать эти свойства внутри скобок. Так было сделано в самом первом примере (компонент Button).
JS-объекты также можно использовать. Один из вариантов использования объектов – передача их вместе со стилями в React-атрибут style:
const ErrorDisplay = ({message}) => <div style={ { color: 'red', backgroundColor: 'yellow' } }> {message} </div>; // Отрисовываем компонент: ReactDOM.render( <ErrorDisplay message="These aren't the droids you're looking for" />, mountNode );
Обратите внимание, что атрибут style здесь является специальным, это не HTML. Объект мы используем как значение этого атрибута.
В JSX можно использовать и элементы React в качестве вызова функции:
const MaybeError = ({errorMessage}) => <div> {errorMessage && <ErrorDisplay message={errorMessage} />} </div>; // Компонент MaybeError использует компонент ErrorDisplay: const ErrorDisplay = ({message}) => <div style={ { color: 'red', backgroundColor: 'yellow' } }> {message} </div>; // Сейчас мы можем использовать компонент MaybeError: ReactDOM.render( <MaybeError errorMessage={Math.random() > 0.5 ? 'Not good' : ''} />, mountNode );
Компонент MaybeError отобразит компонент ErrorDisplay, если в него передать строку (errorMessage). Если строка будет пустой, будет отрисован пустой div.
Внутри JSX возможно использовать функциональные методы для коллекций (map, reduce, concat, filter).
const Doubler = ({value=[1, 2, 3]}) => <div> {value.map(e => e * 2)} </div>; // Отрисовываем компонент ReactDOM.render(<Doubler />, mountNode);
Для React естественно то, как в примере выведено выражение массива в div.
Можно разрабатывать компоненты используя JavaScript-классы
Иногда нужно что-то большее, чем простые функциональные компоненты. React поддерживает создание компонентов с помощью классов JavaScript.
class Button extends React.Component { render() { return <button>{this.props.label}</button>; } } // Отрисовываем компонент (тот же синтаксис) ReactDOM.render(<Button label="Save" />, mountNode);
Класс расширяет React.Component. Он определяет единственную функцию render(), она, в свою очередь, возвращает объект виртуального DOM. Каждый раз при использовании компонента на основе Button, React будет создавать экземпляр компонента в дереве документа.
Поскольку экземпляр создан с использованием компонента, его можно настроить по своему усмотрению с помощью конструктора:
class Button extends React.Component { constructor(props) { super(props); this.id = Date.now(); } render() { return <button id={this.id}>{this.props.label}</button>; } } // Отрисовываем компонент ReactDOM.render(<Button label="Save" />, mountNode);
Также можно определить методы класса и использовать их в любом месте, в том числе внутри JSX.
class Button extends React.Component { clickCounter = 0; handleClick = () => { console.log(`Clicked: ${++this.clickCounter}`); }; render() { return ( <button id={this.id} onClick={this.handleClick}> {this.props.label} </button> ); } } // Отрисовываем компонент ReactDOM.render(<Button label="Save" />, mountNode);
На что нужно обратить внимание в этом примере:
- handleClick() создана с использованием синтаксиса стрелочных функций. Для применения такого синтаксиса необходимо использовать транслятор Babel.
- clickCounter определена с использованием такого же синтаксиса.
- Когда handleClick была указана как значение атрибута onClick, мы не вызвали ее, а передали на нее ссылку.
// Неправильно: onClick={this.handleClick()} // Правильно: onClick={this.handleClick}
События
При обработке событий важно понимать, что все атрибуты элементов React именуются с помощью camelCase. При работе с функциями, мы передаем фактическую ссылку на функцию, а не строку.
React.js создает для DOM-события обертку в виде собственного объекта, чтобы оптимизировать производительность работы с событиями. Внутри обработчика все так же возможно получить доступ ко всем методам, доступным для документа. К примеру, чтобы предотвратить отправку формы по умолчанию, сделаем следующее:
class Form extends React.Component { handleSubmit = (event) => { event.preventDefault(); console.log('Form submitted'); }; render() { return ( <form onSubmit={this.handleSubmit}> <button type="submit">Submit</button> </form> ); } } // Отрисовываем компонент ReactDOM.render(<Form />, mountNode);
Жизненный цикл компонента. Начало
Каждый новый компонент начинает свое существование одинаково. Однако одни коспоненты заканчивают существовать раньше других. Об этом мы еще поговорим, а пока, вот основные этапы жизненного цикла компонента:
- Сначала определяется шаблон React.js для создания элементов из компонента.
- Указывается где он будет использован. К примеру, внутри вызова функции рендера иного компонента или с помощью ReactDOM.render.
- Реакт создает экземпляр элемента и передает ему набор свойств (props), доступ к которым будет доступен через this.props. Эти свойства есть то, что мы передали на втором шаге.
- Поскольку описанное является JavaScript-ом, будет вызван метод конструктора класса (если он определен). Это первый из методов, которые называются методами жизненного цикла компонента.
- React обрабатывает результат вызова функции рендера.
- Затем React осуществит монтирование компонента: взаимодействуя с браузером через DOM API, React выполнит рендеринг.
- Следом, Реакт вызывает другой метод жизненного цикла, который называется componentDidMount. Этот метод можно использовать, чтобы что-то сделать в дереве документа. Весь DOM, с которым мы работали ранее был виртуальным.
- Демонтирование. Жизненный цикл некоторых компонентов заканчивается уже на этом этапе. Компоненты могут быть демонтированы из документа по разным причинам. Однако, перед этим Реакт вызывает другой метод – componentWillUnmount.
При изменении состояния любого элемента в дело по-настоящему вступает React и магия виртуального DOM. То, о чем пойдет речь не является концом жизненного цикла элемента. Но прежде, чем углубиться в это, давайте разберемся с «магией» React.js на данном этапе.
Компоненты имеют внутреннее состояние
Реакт контролирует состояние каждого компонента на случай изменений. Для того, чтобы React действовал эффективно, необходимо изменить поле состояния с помощью API React и функции this.setState.
class CounterButton extends React.Component { state = { clickCounter: 0, currentTimestamp: new Date(), }; handleClick = () => { this.setState((prevState) => { return { clickCounter: prevState.clickCounter + 1 }; }); }; componentDidMount() { setInterval(() => { this.setState({ currentTimestamp: new Date() }) }, 1000); } render() { return ( <div> <button onClick={this.handleClick}>Click</button> <p>Clicked: {this.state.clickCounter}</p> <p>Time: {this.state.currentTimestamp.toLocaleString()}</p> </div> ); } } // Отрисовываем компонент ReactDOM.render(<CounterButton />, mountNode);
Рассмотрим этот пример, начиная с полей класса. Класс имеет два поля. Поле state инициализируется самим объектом, содержащим clickCounter и currentTimestamp.
Второе свойство – функция handleClick, которая передается в событие onClick внутри метода render. Метод handleClick будет изменять состояние экземпляра компонента, используя функцию setState.
Вторым свойством является функция handleClick, которая передается в событие onClick внутри рендера. handleClick() изменяет состояние экземпляра компонента с помощью setState.
Еще одно место, где используется состояние, находится внутри таймера, который был запущен внутри componentDidMount. Каждую секунду он выполняет вызов this.setState.
Состояние было обновлено с использованием двух разных способов:
- Мы передавали функцию, которая возвращает объект – это сделано внутри handleClick.
- Мы передавали простой объект – это сделано в функции обратного вызова, передаваемой в setInterval.
Можно применять оба способа, однако первый предпочтительнее, если вы одновременно читаете и записываете состояние. Внутри функции обратного вызова происходит только запись в state без чтения. Если есть сомнения по поводу того, какой способ использовать – используйте первый. Он не создаст условий для гонки ресурсов, так как setState является асинхронным методом.
Реактивность
React назван так потому, что реагирует на изменения состояния компонентов. Все же он делает это не реактивно, а, скорее, по графику – отсюда появилась шутка, что React следовало бы назвать Schedule.
Однако, если не углубляться внутрь фреймворка, то все что мы видим – это как React реагирует на обновление компонента и автоматически отображает его изменения в дереве документа.
Помните, что входными данными для render() являются свойства (props) и внутреннее состояние, которое может быть обновлено в любое время.
Когда для render меняются входные данные, меняется и результат ее выполнения.
React.js ведет запись жизненного цикла компонента. Когда React.js видит, что один рендер отличается от другого, он переводит разницу между своим виртуальным представлением в операции с DOM API, которые будут отрисованы в документе.
React.js как посредник
О Реакте можно думать как об агенте, которого наняли для общения с браузером. Рассмотрим метку времени на странице как пример. Вместо того, чтобы вручную переходить в браузер и вызывать операции через DOM API, чтобы просто изменить и обновить элемент p#timestamp, нужно просто изменить свойство в состоянии компонента, а React.js уже позаботиться об его отображении.
Жизненный цикл компонента. Конец
Теперь, когда мы знаем, что за магия происходит при изменении состояния компонента, рассмотрим оставшиеся концепции.
- Компонент может быть необходимо повторно отрисовать, если его состояние будет обновлено, либо, если родительский элемент изменит свои свойства.
- Если были изменены свойства, React.js вызовет метод жизненного цикла componentWillReceiveProps.
- Если объект или его свойства были изменены, React.js вызывает еще один метод – shouldComponentUpdate, который, по сути, является вопросом. Так что, если есть необходимость самостоятельно настроить процесс рендера, вы можете ответить на этот вопрос вернув true или false.
- Если shouldComponentUpdate не объявлен, Реакт вызовет безусловный componentWillUpdate и рассчитает различия между текущим компонентом и его новым видом, с учетом изменений.
- Если никаких изменений не зафиксировано, React.js ничего не сделает.
- Если разница есть, фреймворк отрисует компонент.
- Так как процесс обновления в любом случае произошел, Реакт вызовет метод componentDidUpdate.
Определять методы жизненного цикла необязательно. Но они могут пригодиться для анализа поведения приложения и оптимизации его производительности.