Skip to main content

Вопросы по React. Версия 2

Источник  👀

React, React Router, библиотеки для React-приложений

Что такое React?

React - это JavaScript-библиотека, предназначенная для создания быстрых и интерактивных пользовательских интерфейсов (user interfaces, UI) для веб- и мобильных приложений. Это открытая (с открытым исходным кодом), основанная на компонентах, библиотека для фронтенда, отвечающая только за слой представления (view) приложения.

Основная задача React - разработка быстрых пользовательских интерфейсов. В нем используется виртуальная объектная модель документа (virtual document object model (DOM) - программный интерфейс приложения (application programming interface (API)), для HTML и XML-документов. Он определяет логическую структуру документа, способы доступа к документу и управления им. Виртуальный DOM - это JavaScript-объект, что повышает производительность приложения. Виртуальный DOM быстрее обычного (браузерного, реального или настоящего). Мы можем использовать React как на стороне клиента, так и на стороне сервера, а также вместе с другими фреймворками. В нем используются компоненты и различные паттерны проектирования для работы с данными, что улучшает читаемость кода и облегчает поддержку больших приложений.

Читать подробнее

Как React работает?

Разрабатывая клиентские приложения, команда разработчиков Facebook осознала, что DOM является медленным. Для того, чтобы сделать его быстрее, React использует виртуальный DOM, который, по сути, является представлением DOM-дерева в JavaScript. Когда возникает необходимость чтения или записи в DOM, используется данное представление. Затем виртуальный DOM пытается определить наиболее эффективный способ обновления реального DOM.

В отличие от DOM-элементов браузера, создание элементов в React обходится гораздо дешевле (с точки зрения производительности). Виртуальный DOM заботится об обновлении настоящего для полного совпадения с React-элементами. Это объясняется тем, что JavaScript очень быстрый, и хранение DOM-дерева в виде объекта ускоряет модификацию последнего.

Что такое компонент?

Components Tree

Компоненты (components) - это основные строительные блоки любого React-приложения. Как правило, приложение на React состоит из множества компонентов. Проще говоря, компонент - это JavaScript-класс или функция, опционально принимающие так называемые пропы (свойства, properties, props) и возвращающие элемент React, описывающий, как должна выглядеть определенная часть UI.

React-приложение состоит из множества компонентов, каждый из которых отвечает за отрисовку (рендеринг, render) небольшой, переиспользуемой (reusable) части приложения. Компоненты могут вкладываться в другие компоненты, что обеспечивает возможность создания сложных приложений, состоящих из элементарных "кирпичиков" (это называется "композицией компонентов"). Также компоненты могут поддерживать внутреннее состояние - например, компонент TabList может хранить переменную, значением которой является открытая вкладка.

class Welcome extends React.Component {
render() {
return <h1>Привет, народ!</h1>
}
}

Назовите преимущества и ограничения React

React Features

Преимущества

  • Использование виртуального DOM для определения того, какие части UI подверглись изменениям, и повторный рендеринг только этих частей в реальном DOM существенно повышают производительность приложения.
  • JSX (JavaScript и XML) делает код компонентов/блоков более читаемым. Он отчетливо показывает, как компоненты связаны (скомбинированы) между собой.
  • Связывание данных в React предоставляет хорошие условия для создания динамичных приложений.
  • Быстрый рендеринг. Использование встроенных методов для минимизации количества операций с DOM помогает оптимизировать и ускорить процесс обновления компонентов.
  • Тестируемость. React предоставляет отличные встроенные инструменты для тестирования и отладки кода.
  • Дружелюбность по отношению к SEO (search engine optimization - поисковая оптимизация). React предоставляет возможность рендеринга страниц на стороне сервера и регистрации обработчиков событий на стороне клиента: React.renderToString() вызывается на сервере; React.render() вызывается на клиенте; _ React сохраняет разметку, сгенерированную на сервере, и добавляет к ней обработчики событий.

Ограничения

  • Кривая обучения. Будучи библиотекой, а не полноценным фреймворком, React требует глубоких знаний по внедрению UI во фреймворки MVC (Model-View-Controller - Модель-Представление-Контроллер).
  • Одним из недостатков React также является ориентированность на слой представления. Для решения проблем "Представления" требуется поиск подходящей "Модели" и "Контроллера".
  • Разработка приложения без использования изоморфного подхода приводит к проблемам с индексацией приложения поисковыми роботами (речь идет о том, что одностраничные приложения (SPA) индексируются хуже статических).

Что такое JSX и как он помогает разрабатывать приложения?

JSX позволяет создавать HTML-элементы прямо в JavaScript и помещать их в DOM без использования таких методов, как createElement или appendChild. JSX преобразует HTML-теги в элементы React. React использует JSX для шаблонизации вместо обычного JavaScript. Использовать JSX не обязательно, однако он предоставляет несколько преимуществ:

  • Он быстрее благодаря оптимизации во время компиляции кода в JavaScript.
  • Он также является "типобезопасным", большинство ошибок перехватываются во время компиляции.
  • Он позволяет легче и быстрее создавать шаблоны.
import React from 'react'

class App extends React.Component {
render() {
return (
<div>
Привет, народ!
</div>
)
}
}
export default App

JSX - это выражения JavaScript. JSX-выражения являются валидными JavaScript-выражениями. После компиляции, они становятся обычными объектами. Например, такой код:

const hello = <h1 className = "greet"> Привет, народ!</h1>

Компилируется в такой:

const hello = React.createElement {
type: "h1",
props: {
className: "greet",
children: "Привет, народ!"
}
}

Поскольку JSX компилируется в объекты, он может использоваться наравне с обычными выражениями JavaScript.

Что такое ReactDOM?

ReactDOM - это пакет (package), предоставляющий специфичные для браузера методы, которые могут быть использованы на верхнем уровне приложения для эффективного управления DOM-элементами, имеющимися на странице. ReactDOM предоставляет в распоряжение разработчиков следующие методы:

  • render.
  • findDOMNode.
  • unmountComponentAtNode.
  • hydrate.
  • createPortal и др.

До версии 0.14 ReactDOM был частью React. Одной из главных причин разделения React и ReactDOM было появление React Native. React содержит функционал, используемый в веб- и мобильных приложениях. Функционал ReactDOM используется только в веб-приложениях.

ReactDOM использует наблюдаемые (observables) объекты, которые предоставляют эффективный способ работы с DOM. ReactDOM может использоваться как на стороне клиента, так и на стороне сервера.

// index.js
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App/App'

ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
)

Для того, чтобы иметь возможность использовать ReactDOM в веб-приложении, написанном на React, мы, прежде всего, должны его импортировать из react-dom:

import ReactDOM from 'react-dom'

ReactDOM.render()

Эта функция используется для рендеринга отдельного компонента React или нескольких компонентов, обернутых в другой компонент, фрагмент (fragment) или контейнер div. Данная функция использует эффективные методы React для обновления DOM, имея возможность обновлять только те части DOM, которые подверглись изменениям. Функция возвращает ссылку на компонент или null, если был отрендерен компонент без состояния.

ReactDOM.render() удаляет всех потомков переданного контейнера, если таковые имеются. Он использует высокоэффективный алгоритм сравнения и способен модифицировать любое поддерево DOM:

ReactDOM.render(element, container, callback)
  • element: JSX-выражение или React-элемент (компонент), который должен быть отрендерен;
  • container: контейнер (HTML-элемент), в котором должен быть отрисован компонент;
  • callback: опциональный параметр - функция, вызываемая после завершения рендеринга.

findDOMNode()

Эта функция, обычно, используется для получения узла DOM, в котором был отрендерен некоторый компонент. Данный метод используется редко, поскольку тоже самое можно сделать с помощью атрибута ref (ссылка, реф), добавленного к компоненту.

findDOMNode() может быть реализован только в отношении смонтированных компонентов, поэтому функциональные компоненты не могут его использовать:

ReactDOM.findDOMNode(component)

Данный метод принимает один параметр - компонент, поиск которого осуществляется в DOM. Функция возвращает DOM-узел, в котором был отрисован компонент (в случае успеха) или null.

unmountComponentAtNode()

Эта функция используется для размонтирования и удаления React-компонента, который был отрендерен в определенном контейнере:

ReactDOM.unmountComponentAtNode(container)

Данный метод принимает единственный параметр - узел DOM, из которого должен быть удален компонент. Функция возвращает true в случае успеха, или false в противном случае.

hydrate()

Эта функция эквивалентна методу render, но используется при рендеринге на стороне сервера:

ReactDOM.hydrate(element, container, callback)
  • element: JSX-выражение или компонент React, который должен быть отрендерен;
  • container: контейнер (HTML-элемент), в котором должен быть отрисован компонент;
  • callback: опциональный параметр - функция, вызываемая после завершения рендеринга.

Данная функция пытается зарегистрировать обработчики событий для существующей разметки и возвращает ссылку на компонент или null в случае рендеринга компонента без состояния.

createPortal()

Обычно, когда элемент возвращается из метода render компонента, он монтируется в DOM как потомок ближайшего родительского узла, что в некоторых случаях является нежелательным. Порталы (portals) позволяют рендерить компонент в узле DOM, который находится за пределами текущего дерева DOM родительского компонента:

ReactDOM.createPortal(child, container)
  • child: JSX-выражение или React-компонент для рендеринга;
  • container: контейнер (HTML-элемент), в котором должен быть отрисован компонент.

В чем разница между ReactDOM и React?

import React from 'react'
import ReactDOM from 'react-dom'

class MyComponent extends React.Component {
render() {
return <div>Привет, народ!</div>
}
})

ReactDOM.render(<MyComponent />, someDomNode)

Пакет React содержит такие методы, как React.createElement, React.createClass, React.Component, React.Children и т.д.

Пакет ReactDOM содержит такие методы, как ReactDOM.render, ReactDOM.unmountComponentAtNode, ReactDOM.findDOMNode, а также react-dom/server, включающий методы ReactDOMServer.renderToString и ReactDOMServer.renderToStaticMarkup.

Модуль ReactDOM содержит специфичные для DOM методы, в то время как React включает основные инструменты для разных платформ (например, React Native).

В чем разница между классовыми и функциональными компонентами?

Функциональные компоненты

  • Функциональные компоненты - это обычные функции JavaScript. Чаще всего, они представлены в форме стрелочных функций, но их вполне можно создавать и с помощью ключевого слова function.
  • Их часто называют компонентами без состояния, которые просто принимают данные и отображают их в некоторой форме, поэтому они, в основном, отвечают за рендеринг UI (так было до появления хуков).
  • В них нельзя использовать методы жизненного цикла, например, componentDidMount (в настоящее время хуки предоставляют альтернативы почти всем методам жизненного цикла).
  • У них нет метода render.
  • Как правило, они отвечают за UI и форму представления данных (например, компонент кнопки).
  • Принимают и используют пропы.
  • Им следует отдавать предпочтение в случаях, когда не требуется работать с состоянием (так было до появления хуков).
const ClockUsingHooks = props => {
const [time, setTime] = useState(new Date())

useEffect(() => {
const tick = setInterval(() => {
setTime(new Date())
}, 1000)
return () => { clearInterval(tick) }
}, [])

return (
<div className="clock">
<h1>Привет! Это часы, созданные с помощью функционального компонента</h1>
<h2>Сейчас {time.toLocaleTimeString()}</h2>
</div>
)
}

export default ClockUsingHooks

Классовые компоненты

  • Для создания классовых компонентов используются классы ES6, расширяющие класс React.Component.
  • Их часто называют компонентами с состоянием, поскольку в них реализуется логика поведения на основе некоторого состояния.
  • Внутри классов могут использоваться методы жизненного цикла, например, componentDidMount.
  • Принимают props и имеют к ним доступ через this.props.
  • Могут содержать refs (ссылки, рефы) на нижележащие DOM-узлы.
  • Могут использовать такие техники улучшения производительности, как shouldComponentUpdate() и PureComponent
class ClockUsingClass extends React.Component {
constructor(props) {
super(props)
this.state = { date: new Date() }
}

componentDidMount() {
this.time = setInterval(() => {
this.changeTime()
}, 1000)
}

componentWillUnmount() {
clearInterval(this.time)
}

changeTime() {
this.setState({ date: new Date() })
}

render() {
return (
<div className="clock">
<h1>Привет! Это часы, созданные с помощью классового компонента</h1>
<h2>Сейчас {this.state.date.toLocaleTimeString()}</h2>
</div>
)
}
}

export default ClockUsingClass

В чем разница между состоянием и пропами?

Состояние (state) - это данные, содержащиеся внутри компонента. Состояние является локальным (принадлежащем определенному компоненту). Компонент может обновлять состояние с помощью метода setState:

class Employee extends React.Component {
constructor() {
this.state = {
id: 1,
name: "Иван"
}
}

render() {
return (
<div>
<p>{this.state.id}</p>
<p>{this.state.name}</p>
</div>
)
}
}

export default Employee

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

class ParentComponent extends Component {
render() {
return (
<ChildComponent name="Потомок" />
)
}
}

const ChildComponent = (props) => {
return <p>{props.name}</p>
}

Разница между состоянием и пропами

ПропыСостояние
Доступны только для чтенияСостояние обновляется асинхронно
Являются иммутабельнымиЯвляется изменяемым, но не напрямую
Позволяют передавать данные от одного компонента другому в качестве аргументаСодержит информацию о компоненте
Доступны для дочерних компонентовНедоступно для дочерних компонентов
Используются для взаимодействия между компонентамиИспользуются для рендеринга динамических изменений компонента
Компонент без состояния может иметь пропыКомпонент без состояния не может иметь пропов
Могут сделать компонент переиспользуемымНе может сделать компонент преиспользуемым
Являются внешними и управляются родительским компонентомЯвляется внутренним и управляется самим компонентом

Как создать компонент высшего порядка?

Higher Order Components

Компонент высшего порядка (Higher Order Component, HOC) - это функция, принимающая компонент и возвращающая новый компонент. Это продвинутая техника, позволяющая повторно использовать логику компонента. HOC не являются частью React API. HOC является паттерном, производным от композиционной природы React. Компонент преобразует пропы в UI, а HOC трансформирует один компонент в другой. Примерами популярных HOC являются методы connect в Redux и createContainer в Relay.

// HOC.js
import React, { Component } from 'react'

export default function Hoc(WrappedComponent) {
return class extends Component {
render() {
return (
<div>
<WrappedComponent></WrappedComponent>
</div>
)
}
}
}
// App.js
import React, { Component } from 'react'
import Hoc from './HOC'

class App extends Component {
render() {
return (
<div>
Компонента высшего порядка
</div>
)
}
}

App = Hoc(App)

export default App

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

  • Мы не модифицируем компоненты, а создаем новые.
  • HOC используются для композиции компонентов в целях обеспечения возможности повторного использования кода.
  • HOC являются "чистыми" (pure) функциями. Они не имеют побочных эффектов (side effects) и всегда возвращают одинаковые результаты для одних и тех же аргументов.

Что такое "чистый" компонент?

Чистые компоненты (pure components) - это компоненты, которые не рендерятся повторно при обновлении их состояния или пропов одними и теми же значениями. Если значение предыдущего и нового состояния и пропов равны, компонент не повторно отрисовывается. Чистые компоненты ограничивают повторный рендеринг, обеспечивая повышение производительности приложения.

Особенности чистых компонентов

  • Предотвращают повторный рендеринг компонента, если его состояние и пропы остались прежними.
  • Неявно реализуют метод shouldComponentUpdate.
  • state и props сравниваются поверхностно (shallow).
  • В ряде случаев такие компоненты являются более производительными, чем обычные.

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

Чистые компоненты похожи на обычные, за исключением того, что они неявно реализуют метод shouldComponentUpdate, проводя поверхностное сравнение состояния и пропов. Если текущие и следующие состояние и пропы являются одинаковыми, повторный рендеринг компонента не выполняется.

React-компоненты перерисовываются в следующих случаях:

  1. В компоненте вызывается setState().
  2. Обновляются значения props.
  3. Вызывается forceUpdate().

Чистые компоненты не перерисовываются вслепую, без оценки значений state и props. Если обновленные значения аналогичны предыдущим, повторный рендеринг не запускается.

Компонент без состояния

import { pure } from 'recompose'

export default pure ((props) => <p>Компонент без состояния</p>)

Компонент с состоянием

import React, { PureComponent } from 'react'

export default class Test extends PureComponent{
render() {
return <p>Компонент с состоянием</p>
}
}

Пример

class Test extends React.PureComponent {
constructor(props) {
super(props)
this.state = {
taskList: [
{ title: 'Изучить React'},
{ title: 'Изучить TypeScript'},
{ title: 'Изучить Node.js'},
]
}
}

componentDidMount() {
setInterval(() => {
this.setState((oldState) =>
({ taskList: [...oldState.taskList] })
)
}, 1000)
}

render() {
console.log("Запущен рендеринг списка задач")

return (
<div>
{this.state.taskList.map((task, i) =>
<Task
key={i}
title={task.title}
/>
)}
</div>
)
}
}

class Task extends React.Component {
render() {
console.log("Задача добавлена в список")

return (
<h2>
{this.props.title}
</h2>
)
}
}
ReactDOM.render(<Test />, document.getElementById('app'))

Зачем использовать "чистые" компоненты? Когда следует использовать "чистые" компоненты вместо обычных?

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

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

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

Это как раз то, для чего предназначены "чистые" компоненты (pure components) - они останавливают "порочный круг" рендеринга. "Чистые" компоненты не перерисовываются до тех пор, пока не изменятся их пропы и состояние.

Случаи использования чистых компонентов:

  • В целях предотвращения повторного рендеринга компонента, чьи состояние и пропы остались прежними.
  • Состояние и пропы компонента являются иммутабельными.
  • Мы не планируем реализовывать собственный метод жизненного цикла shouldComponentUpdate.

С другой стороны, мы не должны использовать PureComponent в случаях, когда:

  • Состояние или пропы являются изменяемыми.
  • Мы планируем реализовать собственный метод shouldComponentUpdate.

Почему виртуальный DOM является более эффективным, чем "грязная" проверка?

Virtual DOM

Виртуальный DOM

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

Виртуальный DOM имеет почти такие же свойства, что и настоящий DOM, но он не может напрямую модифицировать содержимое (контент) страницы. Манипуляции с виртуальным DOM быстрее, поскольку он ничего не обновляет на экране. Простыми словами, работа с виртуальным DOM - это работа с копией реального DOM, не более того.

Обновление виртуального DOM быстрее благодаря следующему:

  1. Использованию эффективного алгоритма определения различий (DOM diffing).
  2. Совмещению (группировке, объединению) операций по обновлению.
  3. Оптимизированному обновлению поддеревьев.
  4. Использованию наблюдаемых (observable) объектов для определения изменений вместо "грязной" проверки (dirty check).

Как работает виртуальный DOM

При рендеринге JSX-элемента обновляется виртуальный DOM. Это происходит очень быстро. После обновления новая версия виртуального DOM сравнивается со старой (снимком (snapshot), выполненным перед обновлением). После этого, React определяет отличия между двумя объектами. Данный процесс называется diffing (определение различий).

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

Если кратко, то вот что происходит при обновлении DOM в React:

  1. Обновляется виртуальный DOM.
  2. Обновленный виртуальный DOM сравнивается с предыдущим.
  3. Определяются различия между версиями виртуального DOM.
  4. Обновляются только изменившиеся элементы реального DOM.
  5. Изменения отображаются на экране.

Почему функция setState является асинхронной?

Даже если состояние компонента обновляется синхронно, его пропы всегда обновляются асинхронно. Это означает, что значение пропов является неопределенным до повторного рендеринга родительского компонента. Объекты, предоставляемые React (state, props, refs), согласованы между собой. Если мы реализуем синхронный setState(), то могут возникнуть проблемы.

setState() не изменяет state сразу, но создает запрос на изменение состояния (планирует или откладывает обновление). Поэтому после обновления state теоретически может иметь старое значение. Несколько операций обновления могут объединяться React в одну в целях повышения производительности.

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

Что такое управляемые и неуправляемые компоненты?

В управляемых компонентах данные из полей формы обрабатываются React-компонентом. Альтернативой являются неуправляемые компоненты, где данные обрабатываются DOM.

Управляемые компоненты

В управляемых компонентах данные формы обрабатываются с помощью состояния компонента. Состояние компонента выступает в роли "единственного источника истины" (single source of truth) для "инпутов".

import React, { Component } from 'react'

class App extends Component {
state = {
message: ''
}

updateMessage = (newText) => {
console.log(newText)

this.setState(() => ({
message: newText
}))
}

render() {
return (
<div className="App">
<div className="container">
<input
type="text"
placeholder="Введите текст сообщения..."
value={this.state.message}
onChange={(event) => this.updateMessage(event.target.value)}
/>
<p>Сообщение: {this.state.message}</p>
</div>
</div>
)
}
}

export default App

Неуправляемые компоненты

Неуправляемые компоненты похожи на обычные HTML-элементы. Данные каждого инпута сохраняются в DOM, а не в компоненте. Вместо обработчиков событий для обновления состояния, в таких компонента используются refs (ссылки, рефы) для извлечения значений из узлов DOM. Ссылки позволяют получать доступ к узлам DOM или элементам React, создаваемым в методе render:

import React, { Component } from 'react'

class App extends Component {
constructor(props){
super(props)
this.handleChange = this.handleChange.bind(this)
this.input = React.createRef()
}

handleChange = (newText) => {
console.log(newText)
}

render() {
return (
<div className="App">
<div className="container">
<input type="text"
placeholder="Введите текст сообщения..."
ref={this.input}
onChange={(event) => this.handleChange(event.target.value)}
/>
</div>
</div>
)
}
}

export default App

Что такое React.cloneElement()?

Функция React.cloneElement возвращает копию переданного ей элемента. В функцию могут передаваться дополнительные пропы и потомки. Она используется, когда родительский компонент хочет добавить или модифицировать пропы потомков.

React.cloneElement(element, props, children)

cloneElement() принимает три аргумента:

  • element: элемент, подлежащий клонированию;
  • props: пропы, передаваемые клонированному элементу;
  • children: мы также можем передавать потомков в клонированный элемент (новые потомки заменяют старых).
import React from 'react'

export default class App extends React.Component {
// рендеринг родительского и дочернего компонентов
render() {
return (
<ParentComp>
<MyButton/>
<br></br>
<MyButton/>
</ParentComp>
)
}
}

// родительский компонент
class ParentComp extends React.Component {
render() {
// новые пропы
const newProp = 'red'
// перебираем потомков,
// клонируем каждого
// и добавляем новый проп
return (
<div>
{React.Children.map(this.props.children,
(child) =>
React.cloneElement(child, { newProp }, null)
)}
</div>
)
}
}

// дочерний компонент
class MyButton extends React.Component {
render() {
return <button style={{ color: this.props.newProp }}>Нажми на меня</button>
}
}

Когда следует использовать React.cloneElement() вместо this.props.children?

Функция React.cloneElement используется, когда потомком является единственный React-элемент.

Почти всегда используется this.props.children. Клонирование используется в более продвинутых сценариях, когда родителю передается элемент, а дочернему компоненту требуется изменить некоторые свойства этого элемента или добавить нечто вроде ref (ссылки) для доступа к "настоящему" DOM-элементу.

React.Children

Поскольку this.props.children может содержать один или несколько элементов, либо не содержать ни одного элемента, его значением является, соответственно, дочерний узел, массив потомков или undefined. Иногда мы хотим преобразовать потомков перед рендерингом, например, добавить проп к каждому элементу. Если мы хотим это сделать, то должны учитывать возможные типы this.props.children.

class Example extends React.Component {
render() {
return (
<div>
<h2>Потомки ({this.props.children.length}):</h2>
{this.props.children}
</div>
)
}
}

class Widget extends React.Component {
render() {
return <div>
<div>Первый <code>пример</code>:</div>
<Example>
<div>1</div>
<div>2</div>
<div>3</div>
</Example>
<div>Второй <code>пример</code>:</div>
<Example>
<div>A</div>
<div>B</div>
</Example>
</div>
}
}

Вывод

Первый пример:
Потомки (3):
1
2
3
Второй пример:
Потомки (2):
A
B

children - это специальное свойство React-компонента, содержащее любого потомка, определенного в нем, т.е. div внутри Example. this.props.children включает потомков из результата рендеринга.

Какой второй опциональный аргумент может быть передан setState() и в чем его назначение?

Таким аргументом является колбек, который вызывается после обновления состояния и повторного рендеринга компонента.

Функция setState является асинхронной, поэтому она принимает колбек в качестве второго параметра. Как правило, лучше использовать какой-нибудь метод жизненного цикла, чем полагаться на этот колбек, но не лишним будет знать о такой возможности.

this.setState(
{ username: 'Alex' },
() => console.log('Обновление состояние завершено и компонент повторно отрисован.')
)

setState() влечет за собой повторный рендеринг до тех пор, пока shouldComponentUpdate() не вернет false. Во избежание лишних рендерингов, setState() следует вызывать только когда новое состояние действительно отличается от предыдущего. Также вызова setState() следует избегать в таких методах жизненного цикла, как componentDidUpdate, поскольку это может привести к запуску бесконечного цикла.

Что такое useState()?

useState - это хук, позволяющий функциональным компонентам обладать состоянием. Раньше это было возможно только в классовых компонентах.

import React, { useState } from 'react'

const App = () => {
const [count, setCount] = useState(0)

function handleIncrease() {
setCount(count + 1)
}

function handleDecrease() {
setCount(count - 1)
}

return (
<div>
Значение счетчика: {count}
<hr />
<div>
<button type="button" onClick={handleIncrease}>
Увеличить
</button>
<button type="button" onClick={handleDecrease}>
Уменьшить
</button>
</div>
</div>
)
}

useState() принимает единственный аргумент - значение начального состояния. В примере таким значением является 0. Хук возвращает массив из двух элементов: count и setCount. Об этих элементах можно думать как о паре геттер/сеттер. Геттер позволяет получать текущее значение состояния, а сеттер - устанавливать новое значение, т.е. обновлять состояние. На самом деле эти элементы могут называться как угодно (поскольку мы имеем дело с деструктуризацией массива). Такой способ именования является распространенным соглашением и отражает суть возвращаемых useState() значений.

Что такое useReducer()?

useReducer - это хук, принимающий функцию-редуктор и начальное состояние приложения в качестве параметров и возвращающий текущее состояние и диспетчер (dispatcher) для отправки (dispatch) операций для обновления состояния.

Несмотря на то, что useState - это базовый хук, а useReducer - продвинутый, на самом деле useState() реализован с помощью useReducer(). Это означает, что useReducer() - это примитив, который может использоваться во всех случаях использования useState(). Редуктор - мощный инструмент, который может использоваться в самых разных сценариях.

Пример

import React, { useReducer } from 'react'

const initialState = 0

const reducer = (state, action) => {
switch (action) {
case 'increment': return state + 1
case 'decrement': return state - 1
case 'reset': return 0
default: return state
}
}

const ReducerExample = () => {
const [count, dispatch] = useReducer(reducer, initialState)

return (
<div>
{count}
<button onClick={() => dispatch('increment')}>+</button>
<button onClick={() => dispatch('decrement')}>-</button>
<button onClick={() => dispatch('reset')}>0</button>
</div>
)
}

export default ReducerExample

Сначала мы определяем начальное состояние и редуктор. Затем передаем их в useReducer(). Хук возвращает текущее значение состояния и диспетчер, который используется для обновления состояния. Когда пользователь нажимает на кнопку, происходит отправка определенной операции в редуктор, который обновляет счетчик на основе операции. Мы может определять столько операций, сколько требуется нашему приложению.

Что такое useContext()?

Context API

React Context API позволяет получать данные на разных уровнях дерева компонентов без их передачи через props:

import React from "react"
import ReactDOM from "react-dom"

// создаем контекст
const NumberContext = React.createContext()
// он возвращает такой объект
// { Provider, Consumer }

function App() {
// используем провайдер для предоставления потомкам
// доступа к данным, содержащимся в контексте
return (
<NumberContext.Provider value={10}>
<Display />
</NumberContext.Provider>
)
}

function Display() {
const value = useContext(NumberContext)

return <div>Ответ: {value}</div>
}

В чем разница между useEffect() и componentDidMount()?

В React при использовании классовых компонентов мы получаем доступ к методам жизненного цикла (таким как componentDidMount, componentDidUpdate и т.д.). В функциональных компонентах альтернативой методам жизненного цикла являются хуки.

componentDidMount() и useEffect() запускаются после монтирования компонента. Тем не менее, useEffect() вызывается после отображения на экране результатов рендеринга. Это означает, что мы можем получить мерцание (flicker) в случае, когда необходимо прочитать DOM и синхронно обновить состояние для получения нового UI.

useLayoutEffect() был спроектирован специально для таких случаев. Он вызывается перед отображением на экране результатов рендеринга, т.е. синхронно. Поэтому useLayoutEffect(fn, []) ближе к componentDidMount(), чем useEffect(fn, []), по времени выполнения.

// использование классового компонента
import React, { Component } from 'react'

export default class SampleComponent extends Component {
componentDidMount() {
// код, выполняемый после монтирования компонента
}
render() {
return <div>foo</div>
}
}

// использование функционального компонента
import React, { useEffect } from 'react'

const SampleComponent = () => {
useEffect(() => {
// код, выполняемый после монтирования компонента
}, [])

return <div>foo</div>
}

Ограничения useEffect()

Когда useEffect() используется для получения данных от сервера:

  • Первый аргумент - это колбек, вызываемый после создания макета и его отрисовки браузером. Выполнение данного колбека не блокирует процесс отрисовки макета браузером (он является асинхронным).
  • Второй аргумент - массив значений или зависимостей (как правило, пропов).
  • Эффект повторно запускается при изменении любого значения в массиве зависимостей.
  • При отсутствии массива зависимостей эффект выполняется при каждом рендеринге.
  • Если массив зависимостей является пустым, эффект выполняется только один раз при монтировании компонента по аналогии с componentDidMount().

Что вы понимаете под ссылками?

Refs (ссылки, рефы) предоставляют доступ к узлам DOM или элементам React, созданным с помощью метода render. Рефы являются ссылками на DOM-элементы или классовые компоненты из родительского компонента.

Refs также предоставляют возможность связывания элементов дочернего компонента с родительским в форме передачи (перенаправления) ссылок (ref forwarding).

class App extends React.Component {
constructor(props) {
super(props)
// создаем ссылку на `DOM-элемент`
this.textInput = React.createRef()
this.state = {
value: ''
}
}

// обновляем состояние с помощью ссылки
handleSubmit = (e) => {
e.preventDefault()
this.setState({ value: this.textInput.current.value})
}

render() {
return (
<div>
<h1>createRef</h1>
{/_ данное значение будет обновлено _/}
<h3>Значение: {this.state.value}</h3>
<form onSubmit={this.handleSubmit}>
{/_ добавляем ссылку к `input` для обновления `h3` его значением _/}
<input type="text" ref={this.textInput} />
<button>Отправить</button>
</form>
</div>
)
}
}

Случаи использования ссылок

  • Установка фокуса, выделение текста или воспроизведение медиа.
  • Запуск императивной анимации.
  • Интеграция со сторонними библиотеками для работы с DOM.

Когда не следует использовать ссылки

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

Что произойдет при вызове setState() в конструкторе?

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

В конструкторе объекту состояния присваивается начальное значение с помощью this.state = {}. Для обновления состояния используется this.setState().

Пример

import React, { Component } from 'react'

class Food extends Component {
constructor(props) {
super(props)

this.state = {
fruits: ['яблоко', 'апельсин'],
count: 2
}
}

render() {
return (
<div className = "container">
<h2> Привет!</h2>
<p> У меня есть {this.state.count} фруктов</p>
</div>
)
}
}

В чем разница между реальным и виртуальным DOM?

Реальный DOM

DOM расшифровывается как "Document Object Model" (объектная модель документа). Реальный DOM предоставляет интерфейс (API) для работы с узлами (nodes). Данный интерфейс включает такие методы как querySelector, textContent, appendChild, removeChild и т.д.

DOM представляет собой древовидную структуру данных. Поэтому изменения и обновления DOM являются достаточно быстрыми. Однако, после обновления элемент и его потомки должны быть повторно отрисованы для обновления слоя представления. Именно это делает DOM медленным.

Виртуальный DOM

Виртуальный DOM - это объектное представление реального DOM, хранимое в памяти. При каждом изменении состояния приложения в React обновляется виртуальный, а не реальный DOM.

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

Почему виртуальный DOM работает быстрее реального?

При добавлении новых элементов в UI создается виртуальное представление дерева элементов. Каждый элемент является узлом этого дерева. При изменении состояния любого из этих элементов создается новый виртуальный DOM. Новое представление сравнивается со старым, определяется разница.

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

Преимущества виртуального DOM

  • Процесс обновления является сбалансированным и оптимизированным.
  • JSX повышает читаемость компонентов/блоков кода.
  • Связывание данных в React предполагает соблюдение некоторых правил для создания динамичных приложений.
  • Идеально подходит для "mobile first" приложений.
  • Умный рендеринг: использование эвристических методов сравнения позволяет минимизировать количество операций обновления.

Virtual DOM

Когда следует использовать стрелочные функции?

Стрелочные функции не переопределяют значение this, а берут его из так называемого "лексического окружения". Это делает использование this в колбеках гораздо более предсказуемым. Использование встроенных стрелочных функций в функциональных компонентах - это хороший способ реализации логики разделения кода (code splitting).

import React from 'react'
import ReactDOM from 'react-dom'

class Button extends React.Component {
render() {
return (
<button onClick={this.handleClick} style={this.state}>
Сделать фон красным
</button>
)
}

handleClick = () => {
this.setState({ backgroundColor: 'red' })
}
}

ReactDOM.render(
<Button />,
document.getElementById('root')
)
  1. При использовании this на каждом рендеринге создается новая функция.
  2. Это исключает возможность предотвращения ненужных повторных рендерингов при расширении PureComponent() для создания классового компонента.

Определите разницу между компонентами с состоянием и компонентами без состояния

Компоненты с состоянием и без называются по разному:

  • Компоненты-контейнеры и компоненты-представители.
  • Умные и глупые компоненты.

Разница между ними, как следует из названия, состоит в том, что одни компоненты имеют состояние, а другие нет. Это означает, что компоненты с состоянием следят за изменением данных, а компоненты без состояния рендерят то, что передается им с помощью пропов, т.е. они всегда рендерят одно и тоже.

Stateful/Container/Smart компонент:

class Main extends Component {
constructor() {
super()
this.state = {
books: []
}
}

render() {
return <BooksList books={this.state.books} />
}
}

Stateless/Presentational/Dumb компонент:

const BooksList = ({ books }) =>
(<ul>
{books.map((book) => <li key={book.id}>{book.name}</li>})}
</ul>)

Функциональные компоненты или компоненты без состояния

  • Похожи на "чистые" функции в JavaScript.
  • Часто являются компонентами без состояния.
  • Получают пропы от предков и возвращают JSX-элементы.
  • Не имеют методов жизненного цикла и состояния.

Классовые компоненты или компоненты с состоянием

  • Обычно, являются компонентами с состоянием.
  • Имеют методы жизненного цикла и состояние.
  • Могут модифицировать состояние.

Случаи использования компонентов без состояния

  • Простая визуализация пропов.
  • Не требуется состояние или внутренние переменные.
  • Создаваемый элемент не является интерактивным.
  • Требуется переиспользуемый код.

Случаи использования компонентов с состоянием

  • Создаваемый элемент должен быть интерактивным (отвечать на действия пользователя и т.д.).
  • Результат рендеринга зависит от состояния (запрос данных, влияющих на рендеринг, и т.п.).
  • Результат рендеринга зависит от данных, которые не могут быть переданы в виде пропов.

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

Назовите стадии жизненного цикла компонента

React component lifecycle

React предоставляет несколько методов, уведомляющих нас о происходящих процессах. Эти методы называются методами жизненного цикла (lifecycle methods) компонента. Они вызываются в определенном порядке. Жизненный цикл компонента делится на 4 стадии.

  1. Mounting (монтирование).

При создании и встраивании компонента в DOM вызываются следующие методы:

  • constructor()
  • getDerivedStateFromProps()
  • render()
  • componentDidMount()

constructor()

constructor() вызывается при инициализации компонента. Это отличное место для присвоения начального значения объекту состояния и других настроек компонента.

constructor() вызывается с props в качестве аргумента и мы всегда должны начинать с вызова super(props), инициализирующего родительский конструктор, что позволяет потомку наследовать поля и методы предка (React.Component).

getDerivedStateFromProps()

getDerivedStateFromProps() вызывается сразу после рендеринга элемента в DOM. В качестве аргумента он принимает состояние и возвращает объект с его изменениями.

class Color extends React.Component {
constructor(props) {
super(props)
this.state = { color: "red" }
}

static getDerivedStateFromProps(props, state) {
return { color: props.favourColor }
}

render() {
return (
<h1>Мой любимый цвет - {this.state.color}</h1>
)
}
}

ReactDOM.render(<Color favourColor="yellow"/>, document.getElementById('root'))

render()

Данный метод является единственным обязательным методом любого классового компонента. Как следует из его названия, он рендерит разметку для DOM.

componentDidMount()

componentDidMount() вызывается после монтирования компонента.

  1. Updating (обновление).

Следующей стадией жизненного цикла компонента является его обновление. Компонент обновляется при изменении его состояния или пропов.

При обновлении компонента вызываются следующие методы:

  • getDerivedStateFromProps()
  • shouldComponentUpdate()
  • render()
  • getSnapshotBeforeUpdate()
  • componentDidUpdate()

getDerivedStateFromProps()

Это первый метод, вызываемый при обновлении компонента. Это подходящее место для установки объекта состояния на основе начальных пропов.

class Color extends React.Component {
constructor(props) {
super(props)
this.state = { color: "red" }
}

static getDerivedStateFromProps(props, state) {
return { color: props.favourColor }
}

changeColor = () => {
this.setState({ color: "blue" })
}

render() {
return (
<div>
<h1>Мой любимый цвет - {this.state.color}</h1>
<button type="button" onClick={this.changeColor}>Изменить цвет</button>
</div>
)
}
}

ReactDOM.render(<Color favourColor="yellow"/>, document.getElementById('root'))

shouldComponentUpdate()

shouldComponentUpdate() возвращает логическое значение, определяющее должен ли React продолжать рендеринг компонента. Значением по умолчанию является true.

class Color extends React.Component {
constructor(props) {
super(props)
this.state = { color: "red" }
}

shouldComponentUpdate() {
return false
}

changeColor = () => {
this.setState({ color: "blue" })
}

render() {
return (
<div>
<h1>Мой любимый цвет - {this.state.color}</h1>
<button type="button" onClick={this.changeColor}>Изменить цвет</button>
</div>
)
}
}

ReactDOM.render(<Color />, document.getElementById('root'))

render()

Данный метод осуществляет повторный рендеринг компонента с учетом изменений.

getSnapshotBeforeUpdate()

В getSnapshotBeforeUpdate() мы получаем доступ к props и state компонента перед обновлением. Это означает, что даже после обновления мы можем увидеть, какими были пропы и состояние ранее.

getSnapshotBeforeUpdate() должен использоваться совместно с componentDidUpdate(). В противном случае, будет выброшено исключение.

class Color extends React.Component {
constructor(props) {
super(props)
this.state = { color: "red" }
}

componentDidMount() {
setTimeout(() => {
this.setState({ color: "yellow" })
}, 1000)
}

getSnapshotBeforeUpdate(prevProps, prevState) {
document.getElementById("div1").innerHTML =
"Предыдущим значением значением 'color' было " + prevState.color
}

componentDidUpdate() {
document.getElementById("div2").innerHTML =
"Текущим значением 'color' является " + this.state.color
}

render() {
return (
<div>
<h1>Мой любимый цвет - {this.state.color}</h1>
<div id="div1"></div>
<div id="div2"></div>
</div>
)
}
}

ReactDOM.render(<Color />, document.getElementById('root'))

componentDidUpdate()

componentDidUpdate() вызывается после применения обновлений к DOM.

class Color extends React.Component {
constructor(props) {
super(props)
this.state = { color: "red" }
}

componentDidMount() {
setTimeout(() => {
this.setState({ color: "yellow" })
}, 1000)
}

componentDidUpdate() {
document.getElementById("mydiv").innerHTML =
"Обновленным значением 'color' является " + this.state.color
}

render() {
return (
<div>
<h1>Мой любимый цвет - {this.state.color}</h1>
<div id="mydiv"></div>
</div>
)
}
}

ReactDOM.render(<Color />, document.getElementById('root'))
  1. Unmounting (размонтирование)

Следующей стадией жизненного цикла компонента является его удаление из DOM или размонтирование в терминологии React.

componentWillUnmount()

Нажатие кнопки удаляет header:

class Container extends React.Component {
constructor(props) {
super(props)
this.state = { show: true }
}

removeHeader = () => {
this.setState({ show: false })
}

render() {
let myHeader

if (this.state.show) {
myHeader = <Child />
}

return (
<div>
{myHeader}
<button type="button" onClick={this.removeHeader}>Удалить "шапку"</button>
</div>
)
}
}

class Child extends React.Component {
componentWillUnmount() {
alert("Размонтирование.")
}

render() {
return (
<h1>Привет, народ!</h1>
)
}
}

ReactDOM.render(<Container />, document.getElementById('root'))

Для чего нужны ключи?

Ключи (keys) помогают React определять, какие элементы подверглись изменениям, были добавлены или удалены. Проп key должен быть присвоен каждому элементу массива для обеспечения его стабильности.

function NumberList(props) {
const numbers = props.numbers

const listItems = numbers.map((number) =>
<li key={number.toString()}>
{number}
</li>
)

return (
<ul>{listItems}</ul>
)
}

const numbers = [1, 2, 3, 4, 5]

ReactDOM.render(
<NumberList numbers={numbers} />,
document.getElementById('root')
)

Случаи безопасного использование индексов элементов в качестве ключей

  • Список является статичным, т.е. никогда не меняется.
  • Порядок расположения элементов также не меняется.
  • Список не будет фильтроваться (добавление/удаление элементов из списка).
  • Элементы не имеют идентификаторов или других уникальных значений.

Обратите внимание: использование индексов в качестве ключей может привести к непредсказуемому поведению компонента.

Что такое React Router? Для чего в React Router 4 используется ключевое слово switch?

React Router реализует основанный на компонентах подход к маршрутизации. Он предоставляет различные компоненты, связанные с роутингом, для нужд приложения и платформы. React Router обеспечивает синхронизацию UI с URL (адресом страницы). Он имеет простой API с мощными возможностями, такими как "ленивая" (отложенная) загрузка, динамический поиск совпадения с маршрутом, обработка разных способов переключения между страницами и т.д.

yarn add react-router-dom
# или
npm i react-router-dom
import React, { Component } from 'react'
import { BrowserRouter as Router, Route, Redirect, Switch } from 'react-router-dom'

import Todos from './components/Todos/Todos'
import TodosNew from './components/TodosNew/TodosNew'
import TodoShow from './components/TodoShow/TodoShow'

class Router extends Component {
constructor(props) {
super(props)
}

render() {
return (
<Router>
<Switch>
<Route path='/todos/new' component={ TodosNew } />
<Route path='/todos/:id' component={ TodoShow } />
<Route exact path='/' component={ Todos } />
<Redirect from='_' to='/' />
</Switch>
</Router>
)
}
}

export default Router

<Router />

Компонент <Router /> оборачивает маршруты (routes) приложения. Внутрь этого компонента помещаются компоненты <Route />, содержащие ссылки на другие страницы.

<Switch />

Данный компонент позволяет рендерить компонент по совпавшему маршруту или резервный контент при отсутствии совпадения. <Switch> возвращает первый совпавший маршрут.

exact

Атрибут exact указывает на необходимость точного совпадения с маршрутом.

Назовите стандартный набор инструментов для разработки приложений на React

Обычно, мы используем сборщики модулей, такие как Grunt, Watchify/Browserify, Broccoli или Webpack для наблюдения за файловой системой (за добавлением, удалением или редактированием файлов). Кроме того, сборщик настраивается для выполнения группы последовательных или параллельных вспомогательных задач.

К числу таких задач относится следующее:

  • Линтинг (linting) - "линтеры", такие как ESLint или Prettier помогают обеспечить соблюдение определенных правил написания кода (структура, внешний вид и т.д.).
  • Управление зависимостями (dependencies management) - редкий JavaScript-проект обходится без использования сторонних библиотек из реестра npm; для сборщиков (Webpack) и транспиляторов (Babel) существуют специальные плагины, автоматически устанавливающие импортируемые пакеты.
  • Транспиляция (transpilation) - разновидность компиляции, преобразование кода из одной версии в другую с сохранением функционала (например, из ES6 в ES5).
  • Компиляция (compilation) - процесс, следующий за транспиляцией, представляет собой включение статических ресурсов (стилей, изображений, шрифтов и т.п.) в файл с кодом. Данный процесс также может включать в себя анализ и оптимизацию кода.
  • Минификация и сжатие (minification and compression) - как правило, является частью компиляции, уменьшение размеров файлов за счет удаления пробелов, комментариев, сокращения названий переменных и функций и т.д. Для сжатия используются специальные алгоритмы (deflate, brotli и др.).
  • Создание карты источников (source-mapping) - еще одна опциональная часть процесса компиляции, заключающаяся в генерации так называемой карты источников или ресурсов, позволяющих проводить построчное сравнение исходного и результирующего кода.

Как React обрабатывает или ограничивает использование пропов определенного типа?

PropTypes - хороший способ перехвата ошибок, связанных с неправильными типами props. PropTypes позволяет помечать пропы как обязательные или определять их значения по умолчанию.

Пример

import React from 'react'
import PropTypes from 'prop-types'

const Person = (props) => (
<div>
<h1>{props.firstName} {props.lastName}</h1>
{props.country ? <p>Страна: {props.country}</p> : null}
</div>
)

Person.propTypes = {
firstName: PropTypes.string,
lastName: PropTypes.string,
country: PropTypes.string
}

export default Person

PropTypes определяет тип пропа. Каждый раз, когда через проп передается какое-либо значение, оно проверяется на правильный тип. Если будет обнаружен неправильный тип, в консоль будет выведено сообщение об ошибке.

Что такое "бурение пропов" и как его избежать?

В React пропы передаются в одном направлении, сверху вниз, от родительского компонента к дочернему, и последовательно. При наличии незначительного количества пропов или потомков - это не является проблемой. Однако при росте приложения для того, чтобы передать пропы с верхнего уровня приложения компонентам, находящимся на 3 или 4 уровне вложенности, нам приходится передавать одни и те же пропы на каждом уровне дерева компонентов. Это называется бурением пропов (prop drilling).

Context API

Контекст решает некоторые проблемы, связанные с "бурением". Он позволяет компонентам получать данные на любом уровне без их передачи в виде пропов. Передаваемыми данными может быть что угодно: состояние, функция, объект и т.д. Эти данные доступны любым вложенным компонентам в пределах области видимости контекста.

import React from "react"
import ReactDOM from "react-dom"

// создаем контекст
const NumberContext = React.createContext()
// он возвращает объект с двумя значениями
// { Provider, Consumer }

function App() {
// используем провайдер для предоставления потомкам
// доступа к данным
return (
<NumberContext.Provider value={10}>
<div>
<Display />
</div>
</NumberContext.Provider>
)
}

function Display() {
// извлекаем значение из контекста
const value = useContext(NumberContext)

return <div>Ответ: {value}.</div>
}

Как реализовать однократное выполнение компонентом некоторой операции при первоначальном рендеринге?

Для этого можно использовать метод жизненного цикла componentDidMount в классовом компоненте:

class Homepage extends React.Component {
componentDidMount() {
trackPageView('Homepage')
}

render() {
return <div>Домашняя страница</div>
}
}

Любые операции, определенные в componentDidMount(), будут выполнены только один раз при монтировании компонента.

Аналогичный функционал можно реализовать с помощью хука useEffect с пустым массивом зависимостей:

const Homepage = () => {
useEffect(() => {
trackPageView('Homepage')
}, [])

return <div>Домашняя страница</div>
}

useEffect() является более гибким, чем componentDidMount(). Он принимает два параметра:

  • Первым параметром является функция обратного вызова, подлежащая выполнению.
  • Опциональный второй параметр - массив, содержащий отслеживаемые переменные (массив зависимостей).

Второй параметр контролирует запуск эффекта:

  • Если второй параметр отсутствует, эффект выполняется при каждом рендеринге.
  • Если массив содержит переменные, то эффект запускается при монтировании компонента, а также при каждом изменении любой переменной.
  • Если массив является пустым, то эффект будет запущен только один раз при монтировании компонента. В этом случае функционал будет аналогичен вызову componentDidMount() в классовом компоненте.

Как автоматизированные инструменты помогают улучшить доступность приложения?

Существует две категории инструментов, которые могут быть использованы для определения проблем с доступностью приложения:

Инструменты статического анализа

"Линтеры", вроде ESLint, могут использоваться совместно со специальными плагинами, например, eslint-plugin-jsx-a11y для анализа React-проектов на уровне компонентов. Статические анализаторы выполняются очень быстро, поэтому "цена" их использования невелика, а преимущества очевидны.

Инструменты браузера

Браузерные инструменты анализа доступности контента, такие как aXe и Google Lighthouse, осуществляют автоматическую проверку на уровне всего приложения. Они могут обнаружить более "реалистичные" проблемы, поскольку браузеры имитируют поведение пользователя, взаимодействующего со страницей.

Для чего в конструкторе вызывается super(props)?

Ключевое слово super() используется для вызова родительского конструктора. super(props) передает props в родительский конструктор.

class App extends React.Component {
constructor(props) {
super(props)
this.state = {}
}

render() {
return <div>Привет, народ!</div>
}
}

export default App

super(props) вызывает конструктор React.Component, передавая ему props в качестве аргумента. В дальнейшем это позволяет получать доступ к пропам через this.props.

Почему мы не должны обновлять состояние напрямую?

setState() не изменяет state сразу, но откладывает обновление состояния (планирует его обновление). Доступ к state сразу после вызова setState() потенциально может вернуть старое значение.

Синхронизация вызовов setState() не гарантируется и несколько вызовов могут объединяться в один для повышения производительности.

Вызов setState() всегда приводит к повторному рендерингу, за исключением случаев, когда метод shouldComponentUpdate возвращает false.

При прямой модификации state может возникнуть ситуация, когда одни изменения буду перезаписывать другие.

import React, { Component } from 'react'

class App extends Component {
constructor(props) {
super(props)

this.state = {
list: [
{ id: '1', age: 42 },
{ id: '2', age: 33 },
{ id: '3', age: 68 },
],
}
}

onRemoveItem = (id) => {
this.setState((state) => {
const list = state.list.filter((item) => item.id !== id)

return { list }
})
}

render() {
return (
<div>
<ul>
{this.state.list.map((item) => (
<li key={item.id}>
Этому человеку {item.age} лет.
<button
type="button"
onClick={() => this.onRemoveItem(item.id)}
>
Удалить
</button>
</li>
))}
</ul>
</div>
)
}
}

export default App

Для чего используется многоточие (...)?

Для передачи пропов в компонент используются операторы spread (распространения, расширения, распаковки) и rest (оставшиеся, невостребованные, неиспользованные параметры). Рассмотрим пример компонента, ожидающего получить два пропа:

function App() {
return <Hello firstName={firstName} lastName={lastName} />
}

Используем spread-оператор:

function App() {
const props = { firstName: 'Иван', lastName: 'Петров' }

return <Hello {...props} />
}

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

Использование spread-оператора с setState() для определения вложенного состояния

Предположим, что состояние нашего компонента представляется собой объект, свойство которого также является объектом:

this.state = {
stateObj: {
attr1: '',
attr2: ''
}
}

В данном случае мы можем использовать spread-оператор для обновления вложенного объекта состояния:

this.setState((state) => ({
person: {
...state.stateObj,
attr1: 'value1',
attr2: 'value2'
}
}))

Что такое хуки? В чем заключаются преимущества их использования?

Хуки (hooks) - это встроенные функции, которые позволяют использовать состояние и методы жизненного цикла внутри функциональных компонентов. Они не конфликтуют с классовыми компонентами, так что их можно легко внедрить в существующую кодовую базу.

Правила использования хуков

  • Хуки нельзя использовать внутри циклов, условий или вложенных функций.
  • Хуки можно использовать либо внутри компонентов, либо внутри других хуков (в том числе, пользовательских, "кастомных", custom).

Встроенные хуки

Основные (базовые)

  • useState()
  • useEffect()
  • useContext()

Дополнительные (продвинутые)

  • useReducer()
  • useCallback()
  • useMemo()
  • useRef()
  • useImperativeHandle()
  • useLayoutEffect()
  • useDebugValue()

Преимущества хуков

  • С ними легче работать, их легче тестировать (как отдельные функции компонентов), они делают код чище, улучшают его читаемость - сложная логика может быть вынесена в "кастомный" хук.
  • Позволяют разделять сложную логику на маленькие функции, используемые внутри компонентов.
  • Повышают возможность повторного использования кода.
  • Позволяют распределять логику между компонентами через пользовательские хуки.
  • Являются более податливыми к перемещениям в дереве компонентов.

Пример

Классовый компонент:

import React, { Component } from 'react'

class App extends Component {
constructor(props) {
super(props)

this.state = {
isButtonClicked: false
}

this.handleClick = this.handleClick.bind(this)
}

handleClick() {
this.setState((prevState) => ({
isButtonClicked: !prevState.isButtonClicked,
}))
}

render() {
return (
<button
onClick={handleClick}
>
{this.state.isButtonClicked ? 'Кнопка нажата' : 'Нажми на меня'}
</button>
)
}
}

Функциональный компонент:

import React, { useState } from 'react'

const App = () => {
const [isButtonClicked, setIsButtonClicked] = useState(false)

return (
<button
onClick={() => setIsButtonClicked(!isButtonClicked)}
>
{isButtonClicked ? 'Кнопка нажата' : 'Нажми на меня'}
</button>
)
}

Как осуществить валидацию пропов?

Пропы - важный механизм передачи доступных только для чтения значений в React-компоненты. React предоставляет способ проверки пропов с помощью PropTypes. Это позволяет гарантировать, что компоненты получат пропы с правильными типами.

yarn add prop-types
# или
npm i prop-types

Пример

import React from 'react'
import PropTypes from 'prop-types'

App.defaultProps = {
propBool: true,
propArray: [1, 2, 3, 4, 5],
propNumber: 100,
propString: "Привет, React!"
}

class App extends React.Component {
render() {
return (
<>
<h3>Логическое значение: {this.props.propBool ? "Истина" : "Ложь"}</h3>
<h3>Массив: {this.props.propArray}</h3>
<h3>Число: {this.props.propNumber}</h3>
<h3>Строка: {this.props.propString}</h3>
</>
)
}
}

App.propTypes = {
// `isRequired` указывает на то, что проп является обязательным
propBool: PropTypes.bool.isRequired,
propArray: PropTypes.array.isRequired,
propNumber: PropTypes.number,
propString: PropTypes.string,
}

export default App

В чем разница между constructor() и getInitialState()?

Названные подходы не являются взаимозаменяемыми. Мы должны инициализировать состояние в constructor() при использовании ES6-классов и определять метод getInitialState при использовании React.createClass().

Такой код:

import React from 'react'

class MyComponent extends React.Component {
constructor(props) {
super(props)
this.state = { /_ начальное состояние _/ }
}
}

Является эквивалентом такого:

const MyComponent = React.createClass({
getInitialState() {
return { /_ начальное состояние _/ }
}
})

Таким образом, getInitialState() используется с React.createClass(), а constructor() - с React.Component.

Обратите внимание: использовать React.createClass() не рекомендуется.

Как реализовать условное добавление атрибутов?

Встроенные условия в пропах атрибутов:

import React from 'react'

function App() {
const [mood] = React.useState("счастлив")

const greet = () => alert("Приветик!:)")

return (
<button onClick={greet} disabled={"счастлив" === mood ? false : true}>
Сказать "Привет!"
</button>
)
}

Заменяют ли хуки рендер-пропы и компоненты высшего порядка?

Хуки

Хуки позволяют функциональным компонентам иметь состояние и методы жизненного цикла подобно классовым компонентам.

const [value, setValue] = useState(initialValue)

Существует несколько встроенных хуков:

import {
useState,
useEffect,
useReducer,
useCallback,
useMemo,
...
} from 'react'

Компоненты высшего порядка

Компонент высшего порядка (HOC) - это компонент, принимающий компонент в качестве аргумента и возвращающий новый компонент. HOC позволяют создавать композицию компонентов.

import React, { useEffect } from 'react'

const withLogging = (Component) => (props) => {
useEffect(() => {
fetch(`/logger?location=${window.location}`)
}, [])

return <Component {...props} />
}

export default withLogging

Мы можем комбинировать несколько HOC и обернуть ими каждую страницу:

import React from 'react'
import withAuth from './with-auth.js'
import withLogging from './with-logging.js'
import withLayout from './with-layout.js'

const page = compose(
withRedux,
withAuth,
withLogging,
withLayout('default')
)

export default page

Пример использования:

import page from '../hocs/page.js'
import MyPageComponent from './my-page-component.js'

export default page(MyPageComponent)

Как оптимизировать производительность?

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

  1. React DevTools Profiler.

При возникновении проблем с производительностью одного из компонентов, следует начать с "профилировщика" инструментов разработчика React.

React DevTools

  1. Использование метода shouldComponentUpdate.

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

React предоставляет простой метод жизненного цикла для определения того, нуждается ли компонент в повторном рендеринге - shouldComponentUpdate, который запускается перед повторным рендерингом. По умолчанию данный метод возвращает true.

shouldComponentUpdate(nextProps, nextState) {
return true
}

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

shouldComponentUpdate(nextProps, nextState) {
return nextProps.id !== this.props.id
}
  1. Использование "чистых" компонентов.

"Чистые" (pure) компоненты - это компоненты, которые не перерисовываются при обновлении их state и props одними и теми же значениями. Если значения предыдущего state или props и нового state или props являются одинаковыми, компонент не перерисовывается. Это повышает производительность кода.

  1. Использование React.memo().

React.memo() - это компонент высшего порядка. Его функционал похож на React.PureComponent, но он предназначен для функциональных компонентов, а не для классов.

const MyComponent = React.memo((props) => {
/_ рендеринг с помощью пропов _/
})

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

React.memo() проверяет только изменение props. Если в нашем компоненте используются хуки useState или useContext, компонент будет перерисовываться при изменении state или context.

  1. "Виртуализация" длинных списков.

Для решения проблем, связанных с длинной новостной лентой, команда React рекомендует использовать технику под названием windowing. Данная техника заключается в рендеринге только видимых пользователю в настоящий момент элементов списка (+/- определенный отступ), что повышает скорость рендеринга. При прокрутке страницы новые элементы извлекаются и рендерятся. react-window и react-virtualized являются двумя наиболее популярными библиотеками для "виртуализации" длинных списков.

Читать подробнее

Когда следует использовать строгий режим?

StrictMode - это инструмент для определения потенциальных проблем приложения. Как и Fragment, StrictMode ничего не рендерит. Он активирует дополнительные проверки и предупреждения в отношении потомков. Проверки выполняются только в режиме для разработки, они не влияют на производственную сборку.

import React from 'react'

export default function App() {
return (
<Fragment>
<Header />
<React.StrictMode>
<div>
<ComponentOne />
<ComponentTwo />
</div>
</React.StrictMode>
<Footer />
</Fragment>
)
}

В приведенном примере проверки не выполняются по отношению к компонентам <Header> и <Footer>. Тем не менее, <ComponentOne> и <ComponentTwo>, которые являются потомками <React.StrictMode>, будут подвергнуты проверкам.

React.StrictMode для повышения эффективности и поиска потенциальных проблем запускает некоторые методы жизненного цикла и хуки по два раза.Некоторыми из таких методом являются:

  • constructor()
  • render()
  • setState()
  • getDerivedStateFromProps()
  • useState()

Преимущества строгого режима

  • Определение компонентов с небезопасными (unsafe) методами жизненного цикла.
  • Предупреждения об использовании устаревшего API строковых ссылок (string ref).
  • Предупреждения об использовании устаревшего метода findDOMNode.
  • Определение неожиданных побочных эффектов.
  • Определение использования устаревшего API контекста.

Как работает рендеринг при вызове setState()?

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

Метод setState - единственный легитимный способ обновления состояния компонента после инициализации его начального состояния.

import React, { Component } from 'react'

class Search extends Component {
constructor(props) {
super(props)

this.state = {
searchString: ''
}
}
}

Мы передаем пустую строку в качестве начального значения объекта состояния. Для его обновления необходимо вызвать setState():

setState({ searchString: event.target.value })

Здесь мы передаем объект. Объект содержит значение, с помощью которого мы хотим обновить состояние. Обычно, это запускает процесс согласования (reconcilation). Согласование - это способ, с помощью которого React обновляет DOM, применяя изменения к компоненту на основе обновленного состояния.

При вызове setState() React создает новое дерево, содержащее реактивные элементы в компоненте (с обновленным состоянием). Это дерево используется для определения того, как должен измениться интерфейс компонента в ответ на изменение состояния посредством сравнения новых элементов с предыдущим деревом.

Как привязать метод класса к экземпляру?

  1. Использование стрелочной функции.

Для привязки метода класса к экземпляру можно использовать стрелочную функцию:

class Button extends Component {
constructor(props) {
super(props)
this.state = { clicked: false }
}

handleClick = () => this.setState({ clicked: true })

render() {
return <button onClick={this.handleClick}>Нажми на меня</button>
}
}
  1. Привязка в render().
onChange={this.handleChange.bind(this)}

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

  1. Привязка в constructor().
constructor(props) {
super(props)
this.handleChange = this.handleChange.bind(this)
}

Данный подход является рекомендуемым.

В чем разница между состоянием и пропами?

Состояние (state) - это данные, содержащиеся внутри компонента. Эти данные являются локальными и принадлежат определенному компоненту. Компонент сам обновляет состояние с помощью функции setState.

class AppComponent extends React.component {
state = {
msg : 'Привет, народ!'
}

render() {
return <div>Сообщение: {this.state.msg}</div>
}
}

Пропы (props) - это данные, передаваемые в дочерний компонент из родительского. Для дочернего компонента эти данные доступны только для чтения. Тем не менее, в качестве пропа может передаваться колбек, который вызывается внутри потомка для обновления его состояния.

Передача пропа в дочерний компонент:

<ChildComponent color='red' />

Получение доступа к пропу в потомке:

class ChildComponent extends React.Component {
constructor(props) {
super(props)
console.log(props.color)
}
}

Пропы могут использоваться для определения состояния компонента в конструкторе:

class ChildComponent extends React.Component {
constructor(props) {
super(props)
this.state.colorName = props.color
}
}

Пропы не должны изменяться в дочернем компоненте. Они также могут использоваться для предоставления доступа к методам родительского компонента. Это позволяет управлять состоянием потомков в родительском компоненте. Данная техника называется "подъемом состояния" (state lifting).

Разница между состоянием и пропами

ПропыСостояние
Доступны только для чтенияОбновляется асинхронно
Позволяют передавать данные из одного компонента в другой в качестве аргументаСодержит информацию о компоненте
Доступны для дочерних компонентовНе доступно для потомков
Используются для взаимодействия между компонентамиМожет использоваться для рендеринга динамических изменений компонента
Компоненты без состояния могут иметь пропыКомпоненты без состояния не могут иметь состояния
Являются внешними и контролируются родительским компонентомЯвляется внутренним и контролируются самим компонентом

Как создать форму?

App.js:

import React, { Component } from "react"
import countries from "./countries"
import './App.css'

export default function App() {
const [email, setEmail] = React.useState("")
const [password, setPassword] = React.useState("")
const [country, setCountry] = React.useState("")
const [acceptedTerms, setAcceptedTerms] = React.useState(false)

const handleSubmit = (event) => {
console.log(`
Адрес электронной почты: ${email}
Пароль: ${password}
Страна: ${country}
Согласие с условиями: ${acceptedTerms}
`.trim())
event.preventDefault()
}

return (
<form onSubmit={handleSubmit}>
<h1>Создать аккаунт</h1>

<label>
Email:
<input
name="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</label>

<label>
Пароль:
<input
name="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</label>

<label>
Страна:
<select
name="country"
value={country}
onChange={(e) => setCountry(e.target.value)}
required
>
<option key=""></option>
{countries.map((country) => (
<option key={country}>{country}</option>
))}
</select>
</label>

<label>
<input
name="acceptedTerms"
type="checkbox"
onChange={(e) => setAcceptedTerms(e.target.value)}
required
/>
Принять правила игры
</label>

<button>Отправить</button>
</form>
)
}

Countries.js:

export default [
'Austria',
'Denmark',
'France',
'Germany',
'India',
'Italy',
'Poland',
'Russia',
'Sweden',
'United States'
]

Результат:

React Form

Как изменить состояние дочернего компонента с помощью родительского?

Предположим, что у нас имеется два компонента, Parent и Child. Значение состояния Parent зависит от Child. Child имеет поле для ввода текста, значение которого передается в Parent:

function Parent() {
const [value, setValue] = React.useState("")

function handleChange(newValue) {
setValue(newValue)
}

// передаем колбек
return <Child value={value} onChange={handleChange} />
}

function Child(props) {
function handleChange(event) {
// вызываем колбек с новым значением
props.onChange(event.target.value)
}

return <input value={props.value} onChange={handleChange} />
}

Что вы понимаете под термином "опрос"?

Использование setInterval() в React-компонентах позволяет выполнять функцию или другой код через определенные промежутки времени:

useEffect(() => {
const interval = setInterval(() => {
console.log('Это сообщение будет выводиться в консоль каждую секунду')
}, 1000)
return () => clearInterval(interval)
}, [])

В приведенном примере задача выполняется каждую секунду в хуке useEffect. Задача начнет выполняться после монтирования компонента. Для отключения таймера мы возвращаем clearInterval() из useEffect().

Использование setInterval() в компонентах React

Для периодического выполнения задачи мы вызываем setInterval() внутри компонента:

import React, { useState, useEffect } from 'react'

const IntervalExample = () => {
const [seconds, setSeconds] = useState(0)

useEffect(() => {
const interval = setInterval(() => {
setSeconds((seconds) => seconds + 1)
}, 1000)
return () => clearInterval(interval)
}, [])

return (
<div className="App">
<header className="App-header">
{seconds} секунд прошло после монтирования компонента.
</header>
</div>
)
}

export default IntervalExample

В компоненте IntervalExample значение seconds увеличивается на 1 каждую секунду.

В чем разница между элементом, компонентом и экземпляром компонента?

Компонент (component) - это шаблон. Проект или схема (blueprint). Глобальное определение. Он может быть функцией или классом (с методом рендеринга).

Элемент (element) - это то, что возвращается из компонента. Это объект, описывающий виртуальное представление определенного узла DOM, содержащегося в компоненте. В случае с функциональными компонентами, указанный объект возвращается функцией. В классовых компонентах объект возвращается методом render. Элементы React - это не то, что мы видим в браузере. Это всего лишь объекты, хранящиеся в памяти, мы не можем их изменять.

import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'

class MyComponent extends React.Component {
constructor(props) {
super(props)
console.log('Это экземпляр компонента: ', this)
}

render() {
const another_element = <div>Привет, народ!</div>

console.log('Это элемент: ', another_element)

return another_element
}
}

console.log('Это компонент: ', MyComponent)

const element = <MyComponent/>

console.log('Это элемент: ', element)

ReactDOM.render(
element,
document.getElementById('root')
)

Элементы React

Элемент - это обычный JavaScript-объект с определенными методами. Он имеет 4 свойства:

  • type - строковое представление HTML-тега или ссылка на React-компонент;
  • key - строка-идентификатор React-элемента;
  • ref - ссылка на нижележащий узел DOM или экземпляр компонента React;
  • props - объект со свойствами.

Элемент - это не экземпляр React-компонента. Это всего лишь упрощенное "описание" того, как должен выглядеть экземпляр компонента (или тег HTML).

Элемент, описывающий компонент, "не знает", в каком DOM-узле он будет отрендерен - эта связь абстрагирована и определяется в процессе рендеринга.

Элементы могут иметь потомков. Это предоставляет возможность формирования деревьев элементов, представляющих виртуальное дерево DOM.

React-компоненты и их экземпляры

Пользовательские компоненты создаются с помощью React.createClass() или путем расширения React.Component (ES2015). В процессе инстанцирования компонент ожидает получить объект со свойствами и возвращает экземпляр.

Компонент может иметь состояние, ему доступны методы жизненного цикла. Он обязательно должен иметь метод render, возвращающий элемент React (или несколько элементов). Обратите внимание, что мы никогда не создаем экземпляры самостоятельно, это делает React.

В каком методе жизненного цикла следует выполнять AJAX-запросы?

Согласно официальной документации таким методом является componentDidMount, который вызывается после монтирования компонента в DOM. Здесь мы можем использовать setState() для обновления состояния после получения данных от сервера.

import React from 'react'

class MyComponent extends React.Component {
constructor(props) {
super(props)
this.state = {
error: null,
isLoaded: false,
items: []
}
}

componentDidMount() {
fetch("https://api.example.com/items")
.then((res) => res.json())
.then(
(data) => {
this.setState({
isLoaded: true,
items: data.items
})
},
// обратите внимание: важно обрабатывать ошибки здесь,
// а не в блоке `catch()`, если мы не хотим смешивать
// глобальные исключения с ошибками, возникающими в компоненте
(error) => {
this.setState({
isLoaded: true,
error
})
}
)
.catch(console.error)
}

render() {
const { error, isLoaded, items } = this.state

if (error) {
return <div>Ошибка: {error.message}</div>
} else if (!isLoaded) {
return <div>Загрузка...</div>
} else {
return (
<ul>
{items.map((item) => (
<li key={item.name}>
{item.name} {item.price}
</li>
))}
</ul>
)
}
}
}

Что означает обработка событий?

Обработка событий в React похожа на обработку событий в элементах DOM. Однако существует несколько синтаксических отличий:

  • События в React именуются в стиле "camelCase" (верблюжий стиль), а не в нижнем регистре.
  • В JSX мы передаем в обработчик функцию, а не строку. Обратите внимание, что функция, передаваемая в обработчик, не вызывается, а передается как ссылка.
class Toggle extends React.Component {
constructor(props) {
super(props)
this.state = { isToggleOn: true }

// привязываем `this` к экземпляру с помощью `bind()` во избежание потери контекста в обработчике
this.handleClick = this.handleClick.bind(this)
}

handleClick() {
this.setState((state) => ({
isToggleOn: !state.isToggleOn
}))
}

render() {
return (
<button onClick={this.handleClick}>
{this.state.isToggleOn ? 'Вкл' : 'Выкл'}
</button>
)
}
}

ReactDOM.render(
<Toggle />,
document.getElementById('root')
)

Сколько внешних элементов может содержаться в JSX-выражении?

JSX-выражение должно содержать только один внешний (родительский) элемент. Например:

const headings = (
<div id = "outermost-element">
<h1>Заголовок</h1>
<h2>Еще один заголовок</h1>
</div>
)

Обратите внимание: для предотвращения рендеринга лишних DOM-элементов можно воспользоваться компонентом <React.Fragment></React.Fragment> или его сокращенной версией <></>. Сокращенная версия работает не во всех вспомогательных инструментах.

Для чего React сравнивает DOM?

DOM (Document Object Model, объектная модель документа) - это интерфейс, представляющий HTML-документ в форме древовидной структуры (tree) с узлами (nodes). Эта структура позволяет разработчикам работать с узлами, представленными объектами, тем самым модифицируя документ. DOM формируется браузером при загрузке страницы.

Виртуальный DOM похож на браузерный (реальный или настоящий). Это древовидная структура, хранящаяся в памяти, с элементами, представленными в виде объектов. Данное дерево имеет почти все свойства браузерного DOM, за исключением возможности прямого изменения того, что отображается на экране. Это объект, содержащий компоненты приложения, предоставляющий возможность быстрого и эффективного обновления.

При рендеринге JSX-элемента или при изменении состояния элемента, создается новый виртуальный DOM. Функция, отвечающая за создание этого дерева, это функция render (в классовых компонентах). Процесс создания нового виртуального дерева является очень быстрым, поскольку это всего лишь объект и при этом UI не перерисовывается.

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

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

Процесс определения различий между новым виртуальным DOM и старым называется diffing (определение различий). Для сравнения используется эвристический алгоритм со сложностью O(n). В процессе сравнения React вычисляет минимальное количество операций, необходимых для обновления браузерного DOM, игнорируя ненужные изменения. Данный процесс также именуется reconciliation (согласованием).

React реализует эвристический O(n) алгоритм, основываясь на двух допущениях:

  1. Элементы разного типа приводят к формированию разных деревьев.
  2. Разработчик может пометить определенные элементы как стабильные с помощью пропа key.

Для чего предназначен метод жизненного цикла shouldComponentUpdate?

shouldComponentUpdate() позволяет остановить повторный рендеринг компонента, если в его обновлении нет необходимости. По умолчанию React не сравнивает пропы. При обновлении props или state React запускает процесс повторного рендеринга контента соответствующего компонента.

По умолчанию рассматриваемый метод возвращает true. Для того, чтобы остановить рендеринг "обновленного" компонента, необходимо вернуть false:

shouldComponentUpdate(nextProps, nextState) {
console.log(nextProps, nextState)
console.log(this.props, this.state)
return false
}

Предотвращение ненужного рендеринга

Метод shouldComponentUpdate - самый простой способ повысить производительность приложения. Он сравнивает текущие пропы и состояние со следующими и возвращает true, если они отличаются, и false, если они идентичны. Данный метод не вызывается при первоначальном рендеринге, а также при использовании forceUpdate().

Для чего предназначена функция render?

Все React-приложения начинают с корневого узла DOM, который содержит ту часть DOM, которая управляется React. Когда React запускает процесс рендеринга, JSX конвертируется в обычный JavaScript. Функция render является частью жизненного цикла компонента. ReactDOM - это объект, содержащий метод render, который используется для встраивания JSX-контента в DOM.

Обычно, ReactDOM.render() используется для рендеринга компонента уровня приложения, все остальные компоненты являются его потомками. Результат рендеринга каждого компонента определяется в его методе render (в классовых компонентах). Любой JSX-код, содержащийся в этом методе, преобразуется в React.createElement(tag, props, children) перед рендерингом в DOM.

// App.js
import React from 'react'
import './App.css'

function App() {
return (
<div className="App">
Привет, народ!
</div>
)
}

export default App
// index.js
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App/App'

ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
)

Что такое компонент?

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

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

Компонент без состояния

import React from 'react'

const ExampleComponent = (props) => {
return <h1>Добро пожаловать в мир React!</h1>
}

export default class App extends React.Component {
render() {
return (
<div>
<ExampleComponent/>
</div>
)
}
}

В приведенном примере компонент без состояния ExampleComponent, возвращающий элемент h1, встраивается в компонент App.

Компонент с состоянием

import React from 'react'

class ExampleComponent extends React.Component {
constructor(props) {
super(props)
this.state = {
heading: "Заголовок компонента"
}
}

render() {
return (
<div>
<h1>{ this.props.welcomeMsg }</h1>
<h2>{ this.state.heading }</h2>
</div>
)
}
}

export default class App extends React.Component {
render() {
const welcomeMsg = "Добро пожаловать в мир React!"

return (
<div>
<ExampleComponent welcomeMsg={welcomeMsg}/>
</div>
)
}
}

В приведенном примере компонент с состоянием ExampleComponent, содержащий элементы h1 и h2, обернутые в div, встраивается в компонент App. Элемент h1 берет данные из пропов, а элемент h2 - из внутреннего состояния ExampleComponent.

Пропы

Пропы (props) - это опциональные входные данные, передаваемые из родительского компонента в дочерний. Для дочернего компонента они являются иммутабельными, т.е. доступными только для чтения. Они также могут использоваться для отображения статических значений.

Состояние

Состояние (state) - это некоторая информация о компоненте. Данная информация хранится и контролируется самим компонентом. При любом изменении состояния происходит повторный рендеринг компонента.

Жизненный цикл

Каждый компонент имеет методы жизненного цикла (lifecycle methods). Эти методы определяют поведение компонента на разных стадиях жизненного цикла (монтирование - встраивание в DOM, обновление, размонтирование - удаление из DOM).

Как привязать функцию к экземпляру компонента?

Существует несколько способов предоставить функции доступ к свойствам компонента, таким как props и state.

Привязка в конструкторе (ES5)

class App extends Component {
constructor(props) {
super(props)
this.handleClick = this.handleClick.bind(this)
}

handleClick() {
console.log('Произошло нажатие кнопки')
}

render() {
return <button onClick={this.handleClick}>Нажми на меня</button>
}
}

Синтаксис полей класса

class App extends Component {
// обратите внимание: данный синтаксис является экспериментальным и пока не стандартизирован
handleClick = () => {
console.log('Произошло нажатие кнопки')
}

render() {
return <button onClick={this.handleClick}>Нажми на меня</button>
}
}

Привязка в методе render

class App extends Component {
handleClick() {
console.log('Произошло нажатие кнопки')
}

render() {
return <button onClick={this.handleClick.bind(this)}>Нажми на меня</button>
}
}

Обратите внимание: использование Function.prototype.bind() в методе render приводит к созданию функции при каждом рендеринге компонента, что потенциально может повлечь проблемы с производительностью.

Стрелочная функция в методе render

class App extends Component {
handleClick() {
console.log('Произошло нажатие кнопки')
}

render() {
return <button onClick={() => this.handleClick()}>Нажми на меня</button>
}
}

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

Как передать аргументы в обработчик событий или функцию обратного вызова?

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

<button onClick={() => this.handleClick(id)} />

Это является эквивалентом вызова bind():

<button onClick={this.handleClick.bind(this, id)} />

Пример

const A = 65 // `ASCII-код` символа

class Alphabet extends React.Component {
constructor(props) {
super(props)
this.handleClick = this.handleClick.bind(this)
this.state = {
justClicked: null,
letters: Array.from({ length: 26 }, (_, i) => String.fromCharCode(A + i))
}
}

handleClick(letter) {
this.setState({ justClicked: letter })
}

render() {
return (
<div>
Только что была нажата клавиша {this.state.justClicked}
<ul>
{this.state.letters.map((letter) =>
<li key={letter} onClick={() => this.handleClick(letter)}>
{letter}
</li>
)}
</ul>
</div>
)
}
}

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

const A = 65 // `ASCII-код` символа

class Alphabet extends React.Component {
constructor(props) {
super(props)
this.handleClick = this.handleClick.bind(this)
this.state = {
justClicked: null,
letters: Array.from({ length: 26 }, (_, i) => String.fromCharCode(A + i))
}
}

handleClick(e) {
this.setState({
justClicked: e.target.dataset.letter
})
}

render() {
return (
<div>
Только что была нажата клавиша {this.state.justClicked}
<ul>
{this.state.letters.map((letter) =>
<li key={letter} data-letter={letter} onClick={this.handleClick}>
{letter}
</li>
)}
</ul>
</div>
)
}
}

Как предотвратить слишком ранний вызов функции?

Throttle

Данная техника предотвращает вызов функции более одного раза за определенный промежуток времени. В примере ниже обработчик события нажатия кнопки срабатывает не чаще одного раза в секунду:

import { throttle } from 'lodash'

class LoadMoreButton extends React.Component {
constructor(props) {
super(props)
this.handleClick = this.handleClick.bind(this)
this.handleClickThrottled = throttle(this.handleClick, 1000)
}

componentWillUnmount() {
this.handleClickThrottled.cancel()
}

render() {
return <button onClick={this.handleClickThrottled}>Загрузить еще</button>
}

handleClick() {
this.props.loadMore()
}
}

Debounce

Данная техника позволяет обеспечить вызов функции только по истечении определенного времени с момента ее последнего вызова. Это может быть полезным, когда необходимо выполнять "дорогие" вычисления в ответ на событие, которое возникает очень часто (например, событие прокрутки или нажатия клавиши).

В примере ниже ввод текста задерживается на 250 мс:

import { debounce } from 'lodash'

class Searchbox extends React.Component {
constructor(props) {
super(props)
this.handleChange = this.handleChange.bind(this)
this.emitChangeDebounced = debounce(this.emitChange, 250)
}

componentWillUnmount() {
this.emitChangeDebounced.cancel()
}

render() {
return (
<input
type="text"
onChange={this.handleChange}
placeholder="Поиск..."
defaultValue={this.props.value}
/>
)
}

handleChange(e) {
this.emitChangeDebounced(e.target.value)
}

emitChange(value) {
this.props.onChange(value)
}
}

requestAnimationFrame

requestAnimationFrame() - это способ органичного многократного выполнения функции в браузере за оптимальное время с точки зрения рендеринга. Функция, передаваемая в requestAnimationFrame(), вызывается в следующем кадре (frame). В идеале, частота обновления браузера составляет 60 кадров в секунду (frames per second, fps). Тем не менее, в различных устройствах, частота обновления может отличаться от эталона.

Например, "девайс" с частотой обновления 30 fps сможет обработать не более 30 кадров за секунду. Использование requestAnimationFrame() позволяет гарантировать соблюдение "порога" частоты обновления устройства. Установка частоты в 100 кадров в секунду создаст дополнительную нагрузку на браузер, а пользователь все равно не заметит никакой разницы.

import rafSchedule from 'raf-schd'

class ScrollListener extends React.Component {
constructor(props) {
super(props)

this.handleScroll = this.handleScroll.bind(this)

// создаем функцию для планирования обновлений
this.scheduleUpdate = rafSchedule(
(point) => this.props.onScroll(point)
)
}

handleScroll(e) {
// обновляем "планировщик" при возникновении события прокрутки
// при получении множества событий мы обновляем "планировщик" только последним из них
this.scheduleUpdate({ x: e.clientX, y: e.clientY })
}

componentWillUnmount() {
// отменяем ожидающие своей очереди обновления при размонтировании компонента
this.scheduleUpdate.cancel()
}

render() {
return (
<div
style={{ overflow: 'scroll' }}
onScroll={this.handleScroll}
>
<img src="/my-huge-image.jpg" alt="" />
</div>
)
}
}

Что такое согласование?

Согласование (reconciliation) - это процесс, в ходе которого React обновляет DOM.

Разрабатывая приложение, мы создаем дерево компонентов. React берет это дерево, обрабатывает его и создает виртуальный DOM, который хранится в памяти. При обновлении приложения (т.е. при изменении state или props), React берет обновленный виртуальный DOM и сравнивает его с предыдущим. После этого, React определяет, что и каким образом изменилось. Наконец, изменения применяются к браузерному DOM в части касающейся. Это процедура повторяется снова и снова.

Reconciliation

Синхронизация виртуального DOM с браузерным обеспечивается библиотекой ReactDOM. React должен проводить сравнение деревьев очень быстро, поэтому он использует эвристический алгоритм со сложностью O(n). Такая сложность выполнения алгоритма означает, что если у нас имеется 1000 узлов, нам потребуется выполнить всего 1000 сравнений. Данный алгоритм является улучшенной версией алгоритма со сложностью O(n\^3) => для 1000 узлов потребуется 1 млрд. сравнений.

Пример

Создадим простой компонент, в котором складываются два числа. Числа вводятся в соответствующие поля:

class App extends React.Component {
state = {
result: '',
entry1: '',
entry2: ''
}

handleEntry1 = (event) => {
this.setState({ entry1: event.target.value })
}

handleEntry2 = (event) => {
this.setState({ entry2: event.target.value })
}

handleAddition = (event) => {
const firstInt = parseInt(this.state.entry1)
const secondInt = parseInt(this.state.entry2)
this.setState({ result: firstInt + secondInt })
}

render() {
const { entry1, entry2, result } = this.state
return(
<div>
<div>
Результат: { result }
</div>
<span><input type='text' value={entry1} onChange={this.handleEntry1} /></span>
<br />
<br />
<span><input type='text' value={entry2} onChange={this.handleEntry2} /></span>
<div>
<button onClick={this.handleAddition} type='submit'>Сложить</button>
</div>
</div>
)
}
}

ReactDOM.render(<App />, document.getElementById("root"))

При вводе первого числа React создает новое дерево компонентов. Это дерево (виртуальный DOM) содержит новое значение состояния для свойства entry1. Затем React сравнивает новое дерево со старым, определяет различия и применяет их к браузерному DOM. Новое дерево создается при каждом изменении состояния - при вводе чисел в поля и при нажатии кнопки.

Что такое порталы?

Порталы (portals) предоставляют быстрый и удобный способ рендеринга дочернего компонента в узле DOM, находящимся за пределами иерархии DOM родительского компонента.

Обычно, компонент рендерит дерево элементов (как правило, с помощью JSX), React-элемент определяет, как должен выглядеть DOM родительского компонента.

ReactDOM.createPortal(child, container)

Возможности

  • Дочерний компонент передается узлу, находящемуся в другом поддереве компонентов.
  • По умолчанию таким узлом является document.body, но им может быть любой существующий DOM-элемент.
  • Поддерживается рендеринг на стороне сервера.
  • Массивы не требуется оборачивать в div или React.Fragment.
  • Используются компоненты <Portal /> и <PortalWithState />.

Случаи использования

  • Модальные окна.
  • Всплывающие подсказки.
  • Раскрывающиеся меню.
  • Виджеты и т.д.

Пример

// App.js
import React, { Component } from 'react'
import './App.css'
import PortalDemo from './PortalDemo.js'

class App extends Component {
render () {
return (
<div className='App'>
<PortalDemo />
</div>
)
}
}

export default App
// PortalDemo.js
import React from 'react'
import ReactDOM from 'react-dom'

function PortalDemo(){
return ReactDOM.createPortal(
<h1>Это есть портал</h1>,
document.getElementById('portal-root')
)
}

export default PortalDemo