Все фундаментальные принципы React.js, собранные в одной статье

Перевод статьи All the fundamental React.js concepts, jammed into this single Medium article by Samer Buna

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

В этой статье не рассматривается, что такое React, или почему вы должны его изучить. Вместо этого дается практическое введение в основы React.js для тех, кто уже знаком с JavaScript и знает основы DOM API.

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

Принцип № 1: Компоненты — самая важная часть React

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

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

Компонент React — в его простейшей форме — это обычная функция JavaScript:

// Пример 1
// https://jscomplete.com/repl?j=Sy3QAdKHW
function Button (props) {
// Возвращает DOM элемент. Например:
return <button type="submit">{props.label}</button>;
}
// Отрисовываем компонент Button в браузере
ReactDOM.render(<Button label="Save" />, mountNode)

Фигурные скобки, используемые для метки кнопки будут объяснены ниже. Сейчас о них не стоит беспокоиться. ReactDOM также будет объяснен позже.

Второй аргумент для ReactDOM.render — это элемент DOM, который React собирается захватить и контролировать. В jsComplete REPL вы для этого просто можете использовать специальную переменную mountNode.

Обратите внимание на пример 1:

  • Название компонента начинается с заглавной буквы. Это необходимо, так как мы будем иметь дело с сочетанием элементов HTML и элементов React. Названия в нижнем регистре зарезервированы для элементов HTML. На самом деле, попробуйте назвать компонент React просто «button». ReactDOM проигнорирует функцию и отобразит обычную пустую кнопку HTML.
  • Каждый компонент получает список атрибутов, как и HTML-элементы. В React этот список называется props. С функциональным компонентом вы можете назвать этот список как угодно.
  • Функция render принимает что-то, что выглядит как HTML, но при этом помещено в JavaScript. Это не JavaScript и не HTML, и это даже не React.js. Но при этом это что-то настолько популярно, что стало стандартным в приложениях React. Это называется JSX, и это расширение JavaScript. Попробуйте вернуть любой другой элемент HTML внутри указанной выше функции и убедитесь в их поддержке (например, верните элемент ввода текста input).

Принцип № 2: Что такое JSX (What the flux is JSX)?

Пример 1 может быть записан в чистом React.js без JSX следующим образом:

// Пример 2 - React компонент без JSX
// https://jscomplete.com/repl?j=HyiEwoYB-
function Button (props) {
return React.createElement(
"button",
{ type: "submit" },
props.label
);
}
// Чтобы использовать Button вы должны написать что-то наподобие
// этого:
ReactDOM.render(
React.createElement(Button, { label: "Save" }),
mountNode
);

Функция createElement является основной функцией верхнего уровня в React API. Это 1 из 7 функций на этом уровне, которые вам нужно изучить.

Подобно тому, как сам DOM содержит функцию document.createElement для создания элемента, с указанным именем тега, функция React createElement представляет собой функцию более высокого уровня, которая может делать то, что делает document.createElement, но при этом эта функция также может быть использована для создания элемента для представления компонента React.

В отличие от document.createElement, createElement React принимает неограниченное количество аргументов после второго аргумента, для представления дочерних элементов создаваемого элемента. Таким образом createElement фактически создает дерево.

// Пример 3 - React's createElement API
// https://jscomplete.com/repl?j=r1GNoiFBb
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);

Обратите внимание на пример 3:

  • InputForm не является компонентом React. Это всего лишь элемент React. Вот почему мы использовали InputForm, а не <InputForm /> в вызове ReactDOM.render.
  • В функцию React.createElement было передано еще несколько аргументов после первых двух. Её список аргументов, начиная с третьего, содержит список дочерних элементов для создаваемого элемента.
  • Нам удалось вложить вызовы React.createElement, потому что это все JavaScript.
  • Второй аргумент React.createElement может быть null или пустым объектом, если для элемента не нужны атрибуты или свойства.
  • Мы можем смешивать HTML-элемент с компонентами React. Вы можете думать о элементах HTML как о встроенных компонентах React.
  • React API пытается быть как можно ближе к DOM API, поэтому мы используем className вместо класса для элемента ввода. В тайне мы все хотим, чтобы API React стал частью самого DOM API. Потому что, как вы знаете, это намного лучше.

Код выше — это то, что браузер понимает, когда вы подключаете библиотеку React. Браузер не работает с JSX. Тем не менее, мы, люди, хотим видеть и работать с HTML вместо этих вызовов createElement (представьте себе создание веб-сайта с помощью только document.createElement). Вот почему был создан JSX. Вместо того, чтобы написать форму выше вызывая React.createElement, мы можем написать ее используя синтаксис, очень похожий на HTML:

// Пример 4 - JSX (сравните с примером 3)
// https://jscomplete.com/repl?j=SJWy3otHW
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);

Обратите внимание на следующее:

  • Это не HTML, так как мы все еще используем className вместо class.
  • Мы по-прежнему рассматриваем то, что выглядит как HTML, как JavaScript. Обратите внимание на то, как я добавил точку с запятой в конце.

То, что мы написали выше (пример 4), — это JSX. Тем не менее, в браузере мы используем скомпилированную версию (пример 3). Чтобы пример 4 заработал, нам нужно использовать препроцессор для преобразования версии JSX в версию React.createElement.

JSX — это компромисс, который позволяет нам разрабатывать компоненты React используя HTML-подобный синтаксис, что довольно неплохо.

Слово «Flux» в заголовке выше было выбрано для рифмы, но это также название очень популярной архитектуры приложений, популяризированной Facebook. Самой известной реализацией которой является Redux. Flux идеально подходит для реактивной модели React.

JSX, кстати, может использоваться сам по себе. Это не часть React.

Принцип № 3: Вы можете использовать выражения JavaScript в любом месте JSX

Внутри секции JSX вы можете использовать любое выражение JavaScript в паре фигурных скобок.

// Пример 5 - Использование выражений JavaScript в JSX
// https://jscomplete.com/repl?j=SkNN3oYSW
const RandomValue = () => 
<div>
{ Math.floor(Math.random() * 100) }
</div>;
// Отрисовываем компонент:
ReactDOM.render(<RandomValue />, mountNode);

Любое выражение JavaScript может быть помещено внутрь этих фигурных скобок. Это эквивалентно синтаксису интерполяции ${} в шаблонах JavaScript.

При использовании JSX есть одно ограничение. Внутри JSX можно использовать только выражения. Так, например, вы не можете использовать оператор if, но можете использовать тернарное выражение.

Переменные JavaScript также являются выражениями, поэтому, когда компонент получает список свойств (компонент RandomValue их не использует, свойства являются необязательными), вы можете использовать эти свойства внутри фигурных скобок. Мы использовали их в компоненте Button (пример 1).

Объекты JavaScript также являются выражениями. Иногда мы используем объект JavaScript внутри фигурных скобок, что выглядит как двойные фигурные скобки, но в действительности это просто объект внутри фигурных скобок. Один из вариантов использования — это передача объекта со стилями CSS в специальный атрибут style в React:

// Пример 6 - Объект, переданный в специальное свойство style React
// https://jscomplete.com/repl?j=S1Kw2sFHb
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
);

Обратите внимание на то, как я использовал деструктуризацию для получения message из параметра props. Это JavaScript. Также обратите внимание, что атрибут style является специальным (опять же, это не HTML). Мы используем объект как значение атрибута style. Этот объект определяет стили, как будто мы делаем это используя JavaScript (потому что так и есть).

Вы даже можете использовать элемент React внутри JSX, потому что это тоже выражение. Помните, что элемент React является вызовом функции:

// Пример 7 - Использование элемента React внутри {}
// https://jscomplete.com/repl?j=SkTLpjYr-
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 в противном случае. React рассматривает {true} , {false} , {undefined} и {null} действительными дочерними элементами, которые ничего не отображают.

Вы также можете использовать все функциональные методы JavaScript для коллекций (map, reduce, filter, concat и т. д.) внутри JSX. Опять же, потому что они возвращают выражения:

// Пример 8 - Использование метода map массива внутри {}
// https://jscomplete.com/repl?j=SJ29aiYH-
const Doubler = ({value=[1, 2, 3]}) =>
<div>
{value.map(e => e * 2)}
</div>;
// Отрисовываем компонент
ReactDOM.render(<Doubler />, mountNode);

Обратите внимание на то, как я задал для свойства value значение по умолчанию, потому что это всего лишь JavaScript. Обратите внимание, что я вывел выражение массива внутри div. Для React это естественно. Он разместит каждое удвоенное значение в текстовом узле.

Принцип № 4: Вы можете разрабатывать компоненты React используя классы JavaScript

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

// Пример 9 - Создание компонентов с использованием классов JavaScript
// https://jscomplete.com/repl?j=ryjk0iKHb
class Button extends React.Component {
render() {
return <button>{this.props.label}</button>;
}
}
// Отрисовываем компонент (тот же синтаксис)
ReactDOM.render(<Button label="Save" />, mountNode);

Синтаксис class прост. Определите класс, который расширяет React.Component (еще кое-что, что нужно изучить). Класс определяет единственную функцию render(), и эта функция возвращает объект виртуального DOM. Каждый раз, когда мы используем компонент на основе класса Button (например, <Button… /> ), React будет создавать экземпляр объекта этого компонента на основе класса и использовать этот объект в дереве DOM.

Именно по этой причине мы использовали this.props.label внутри JSX в функции render, поскольку каждый компонент получает специальный экземпляр, называемый props, содержащий все значения, переданные этому компоненту при его создании.

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

// Пример 10 - Настройка экземпляра компонента
// https://jscomplete.com/repl?j=rko7RsKS-
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:

// Пример 11 - Использование свойств класса
// https://jscomplete.com/repl?j=H1YDCoFSb
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);

Несколько замечаний, касаемо примера 11:

  • Функция handleClick записывается с использованием нового синтаксиса стрелочной функции в JavaScript. Данный синтаксис все еще находится в stage-2, но по многим причинам это лучший способ доступа к экземпляру, смонтированному компонентом. Но вам нужно использовать такой компилятор, как Babel, чтобы заставить код выше работать. JsComplete REPL имеет предварительно настроенную конфигурацию и поэтому все примеры в данной статье работоспособны.
  • Мы также определили переменную экземпляра clickCounter, используя тот же синтаксис класса.
  • Когда мы указали функцию handleClick как значение специального атрибута onClick React, мы не вызвали ее. Мы передали ссылку на функцию handleClick. Вызов функции на этом уровне является одной из наиболее распространенных ошибок при работе с React.
// Неправильно:
onClick={this.handleClick()}
// Правильно:
onClick={this.handleClick}

Принцип № 5: События в действии: два важных отличия

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

  • Все атрибуты элементов React (включая события) называются с помощью camelCase, а не в нижнем регистре. Это onClick, а не onclick.
  • Мы передаем фактическую ссылку на функцию JavaScript как обработчик события, а не строку. Это onClick = {handleClick} , а не onClick = “handleClick”.

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

// Пример 12 - Работа с обернутыми событиями
// https://jscomplete.com/repl?j=HkIhRoKBb
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);

Принцип №6: У каждого компонента React есть история

Следующее относится только к компоненту класса (к тем компонентам, которые расширяют React.Component). Функциональные компоненты имеют немного другую историю.

  1. Сначала мы определяем шаблон для React для создания элементов из компонента.
  2. Затем мы указываем React где будем его использовать. Например, внутри вызова функции render другого компонента или с помощью ReactDOM.render.
  3. Затем React создает экземпляр элемента и передает ему набор props, к которым мы можем получить доступ с помощью this.props. Эти props — это то, что мы передали на шаге 2.
  4. Поскольку это все JavaScript, то будет вызван метод конструктора (если он определен). Это первый метод из тех, что мы называем методами жизненного цикла компонентов.
  5. Затем React обрабатывает результат вызова функции render (получает виртуальный узел DOM).
  6. Поскольку это первый раз, когда React выполняет рендеринг элемента, React будет взаимодействовать с браузером (от нашего имени, используя DOM API), чтобы отобразить в нем элемент. Этот процесс широко известен как монтирование.
  7. Затем React вызывает другой метод жизненного цикла, называемый componentDidMount. Мы можем использовать этот метод, чтобы, например, сделать что-то в DOM, который, как мы знаем, существует в браузере. До этого метода жизненного цикла, DOM, с которым мы работали, был виртуальным.
  8. Некоторые истории компонентов заканчиваются здесь. Компоненты демонтируются из DOM браузера по разным причинам. Но перед тем как это произойдет, React вызывает другой метод жизненного цикла, componentWillUnmount.
  9. Состояние любого смонтированного элемента может измениться. Родитель этого элемента может быть повторно отрисован. Также смонтированный элемент может получить другой набор props. И здесь начинается магия React и наступает именно тот момент, когда React нам так необходим! Честно говоря, до этого он нам особо и не был нужен.
  10. История этого компонента не заканчивается, но прежде чем продолжить, нам нужно понять, что же это за состояние, о котором я говорю.

Принцип № 7: React компоненты могут иметь внутреннее состояние

Следующее также применимо только к компонентам классам. Упоминал ли я, что некоторые люди называют компоненты только для отображения “глупыми”?

Поле класса state является специальным в любом компоненте классе React. React контролирует состояние каждого компонента для изменений. Но для того, чтобы React сделал это эффективно, мы должны изменить поле состояния с помощью еще одной функции API React, которую нам нужно изучить, это this.setState:

// Пример 13 - setState API
// https://jscomplete.com/repl?j=H1fek2KH-
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);

Это самый важный пример для понимания. Он в основном подытожит ваши фундаментальные знания о React. После этого примера есть еще несколько мелочей, которые вам нужно изучить, но в основном для них достаточно ваших навыков JavaScript.

Давайте рассмотрим пример 13, начиная с полей класса. В данном классе есть два поля. Специальное поле state инициализируется объектом, который содержит clickCounter, который начинается с 0, и currentTimestamp, который начинается с new Date().

Второе свойство — это функция handleClick, которую мы передали в событие onClick для элемента кнопки внутри метода render. Метод handleClick изменяет состояние экземпляра компонента, используя setState. Обратите внимание на это.

Другое место, в котором мы изменяем состояние, находится внутри таймера, который мы запустили внутри метода жизненного цикла componentDidMount. Он тикает каждую секунду и выполняет другой вызов this.setState.

В методе рендеринга мы использовали два свойства, которые есть у нас в state. Для этого нет специального API.

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

  1. Передавая функцию, которая возвращает объект. Мы сделали это внутри функции handleClick.
  2. Передавая обычный объект. Мы сделали это внутри функции обратного вызова, передаваемой в setInterval.

Оба способа приемлемы, но первый предпочтительнее, когда вы одновременно читаете и записываете состояние (что мы и делаем). Внутри функции обратного вызова мы только записываем в state и не читаем его. Если вы сомневаетесь, всегда используйте первый способ. Это безопаснее с условием гонки, потому что setState на самом деле является асинхронным методом.

Как мы обновляем состояние? Мы передаем объект с новым значением того, что хотим обновить. Обратите внимание, что в обоих вызовах setState мы передаем только одно свойство из поля состояния, а не оба. Это вполне нормально, потому что setState фактически объединяет то, что вы передаете (возвращаемое значение функции), с существующим состоянием. Поэтому, не указывая свойство при вызове setState, мы показываем, что не хотим изменять это свойство (и не удаляем его).

Принцип № 8: React всегда наготове

React получил свое название от того, что он реагирует на изменения состояния (хоть и не реактивно, но по графику). Была шутка, что React должен был быть назван Schedule!

Однако то, что мы наблюдаем невооруженным глазом при обновлении состояния любого компонента, — это то, что React реагирует на это обновление и автоматически отражает обновление в DOM браузера (при необходимости).

Думайте о том, что входными данными для функции render являются:

  • props, которые передаются родителем;
  • внутреннее состояние, которое может быть обновлено в любое время;

Когда входные данные функции render меняются, ее результат также может поменяться.

React ведет запись истории рендеринга, и когда он видит, что один рендер отличается от предыдущего, он рассчитывает разницу между ними и эффективно переводит разницу в фактические операции DOM API, которые затем будет выполнены в DOM.

Принцип № 9: React — ваш агент

Вы можете думать о React как об агенте, которого вы наняли, чтобы общаться с браузером. В качестве примера возьмем текущую метку времени. Вместо того, чтобы нам вручную переходить в браузер и вызывать операции DOM API, для того, чтобы каждый раз находить и обновлять элемент временной метки p#timestamp, мы просто изменили свойство в состоянии компонента, и React выполнил свою работу по общению с браузером от нашего имени. Я считаю, что это истинная причина популярности React. Нам не нравится разговаривать с браузером (и так много диалектов языка DOM, на котором он говорит), и React бесплатно вызвался вести переговоры за нас!

Принцип № 10: У каждого компонента React есть история (часть 2)

Теперь, когда мы знаем о состоянии компонента и как при изменении этого состояния происходит какая-то магия, давайте изучим последние несколько концепций.

  1. Компоненту может понадобится повторная отрисовка, когда его состояние будет обновлено или когда его родитель решает изменить props, которые он передал компоненту.
  2. Если происходит последнее, React вызывает другой метод жизненного цикла, componentWillReceiveProps.
  3. Если объект состояния или переданные props изменены, React должен принять важное решение. Должен ли компонент обновляться в DOM? Вот почему он вызывает другой важный метод жизненного цикла, shouldComponentUpdate. Этот метод по сути является вопросом, поэтому, если вам нужно самостоятельно настроить либо оптимизировать процесс отрисовки, вы должны ответить на этот вопрос, вернув либо true, либо false.
  4. Если метод shouldComponentUpdate не объявлен, React по умолчанию делает очень умную вещь, которая, на самом деле, достаточно хороша в большинстве ситуаций.
  5. Во-первых, React вызывает другой метод жизненного цикла, componentWillUpdate. Затем React рассчитает новое отображение компонента и сравнивает его с последним.
  6. Если результат точно такой же, React ничего не делает (нет необходимости разговаривать с браузером).
  7. Если есть разница, React переносит эту разницу в браузер, как мы видели раньше.
  8. В любом случае, поскольку процесс обновления произошел (даже если результат рендеринга был точно таким же), React вызывает последний метод жизненного цикла, componentDidUpdate.

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

Всё. Верьте или нет, но с тем, что вы изучили выше (или с его частью), вы уже можете начать создавать интересные приложения React. Если вы жаждете большего, ознакомьтесь с инструкцией по началу работы с React.js в Pluralsight.

Также для лучшего понимания темы я хотел бы порекомендовать книгу Learning React от Alex and Eve!

Спасибо за прочтение. Если вы нашли эту статью полезной, то, пожалуйста, рекомендуйте её и делитесь ею. Подписывайтесь на Samer Buna чтобы не пропустить новые статьи о React.js и JavaScript.