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
<!-- index.html -->
<body>
<div id="root"></div>
<div id="portal-root"></div>
</body>

Что такое ReactDOMServer?

Объект ReactDOMServer позволяет рендерить компоненты в виде статической разметки. Обычно, данный объект используется на Node.js-серверах:

// `ESM`
import ReactDOMServer from 'react-dom/server'
// `CommonJS`
const ReactDOMServer = require('react-dom/server')

Рендеринг на стороне сервера (server-side rendering, SSR) - это техника, позволяющая отрисовывать клиентские одностраничные приложения (single page application, SPA) на сервере и отправлять их клиенту в виде готовой разметки. Это делает динамические компоненты статическими.

  • Это повышает скорость рендеринга страниц, что улучшает пользовательский опыт.
  • Это улучшает поисковую оптимизацию (search engine optimization, SEO), облегчая индексацию страниц поисковыми роботами.
  • Это повышает доступность метаданных (изображения, заголовок, описание и т.д.), что позволяет пользователям легко делиться приложением в социальных сетях.

Пример

Устанавливаем Express:

yarn add express
# или
npm i express

Файлы в директории build являются статичными, т.е. будут обрабатываться как есть:

// server/server.js
import path from 'path'
import fs from 'fs'

import express from 'express'
import React from 'react'
import ReactDOMServer from 'react-dom/server'

import App from '../src/App'

const PORT = 8080
const app = express()

const router = express.Router()

const serverRenderer = (req, res, next) => {
fs.readFile(path.resolve('./build/index.html'), 'utf8', (err, data) => {
if (err) {
console.log(err)
return res.status(500).send('Возникла ошибка')
}

return res.send(
data.replace(
'<div id="root"></div>',
`<div id="root">${ReactDOMServer.renderToString(<App />)}</div>`
)
)
})
}

router.use('^/$', serverRenderer)

router.use(
express.static(path.resolve(__dirname, '..', 'build'), { maxAge: '30d' })
)

app.use(router)

const PORT = process.env.PORT || 3000
app.listen(PORT, () => {
console.log(`SSR запущен на порту ${PORT}`)
})

После этого, на клиенте, в src/index.js, вместо вызова:

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

Вызываем ReactDOM.hydrate(), который имеет аналогичный функционал, но также добавляет обработчики событий к существующей разметке после загрузки React:

ReactDOM.hydrate(<App />, document.getElementById('root'))

Node.js-код должен быть транспилирован с помощью Babel, поскольку Node.js не умеет работать с JSX.

Babel

yarn add @babel/register @babel/preset-env @babel/preset-react ignore-styles
# или
npm i @babel/register @babel/preset-env @babel/preset-react ignore-styles

Создаем точку входа (entry point) в server/index.js:

require('ignore-styles')

require('@babel/register')({
ignore: [/(node_modules)/],
presets: ['@babel/preset-env', '@babel/preset-react']
})

require('./server')

"Собираем" React-приложение:

# сборка приложения
yarn build
# или
npm run build

# запуск приложения на сервере
node server/index.js

Что произойдет при использовании пропов в начальном состоянии?

Использование пропов для определения состояния в getInitialState() часто приводит к дублированию "источника истины" (source of truth), становится трудно понять, где находятся настоящие данные. Это связано с тем, что getInitialState() вызывается только в момент создания компонента.

Опасность состоит в том, что если props компонента изменятся без обновления компонента, новое значение пропа никогда не отобразится на экране, поскольку функция-конструктор никогда не обновит текущее состояние компонента. Инициализация состояния с помощью props запускается только при создании компонента.

Плохо

В примере ниже значение inputValue никогда не отобразится на экране:

class App extends React.Component {
// функция-конструктор (или `getInitialState()`)
constructor(props) {
super(props)

this.state = {
records: [],
inputValue: this.props.inputValue
}
}

render() {
return <div>{this.state.inputValue}</div>
}
}

Хорошо

Использование пропов в методе render обновит значение:

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

this.state = {
records: []
}
}

render() {
return <div>{this.props.inputValue}</div>
}
}

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

Хуки

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

function useWindowSize() {
const [size, setSize] = useState([0, 0])

useLayoutEffect(() => {
function updateSize() {
setSize([window.innerWidth, window.innerHeight])
}
// добавляем обработчик
window.addEventListener('resize', updateSize)
updateSize()
// удаляем обработчик
return () => window.removeEventListener('resize', updateSize)
}, [])

return size
}

function ShowWindowDimensions(props) {
const [width, height] = useWindowSize()

return <span>Размеры окна: {width} x {height}</span>
}

Класс

import React from 'react'

class ShowWindowDimensions extends React.Component {
state = { width: 0, height: 0 }

updateDimensions = () => {
this.setState({ width: window.innerWidth, height: window.innerHeight })
}
/_
_ Добавляем обработчик
_/
componentDidMount() {
window.addEventListener('resize', this.updateDimensions)
}
/_
_ Удаляем обработчик
_/
componentWillUnmount() {
window.removeEventListener('resize', this.updateDimensions)
}

render() {
return (
<span>Размеры окна: {this.state.width} x {this.state.height}</span>
)
}
}

Как переключиться с HTTP на HTTPS в Create React App?

Для этого можно установить переменную среды HTTPS в значение true при запуске сервера для разработки с помощью yarn start / npm start:

# windows
set HTTPS=true && npm start

Существуют и другие способы.

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

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

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

При использовании уникальных пропов key, React полагается на эти пропы при сравнении компонентов. Если ключ компонента изменился, React создает его заново:

renderContent() {
if (this.state.activeItem === 'item-one') {
return (
<Content title="Первый" key="first" />
)
} else {
return (
<Content title="Второй" key="second" />
)
}
}

Назовите особенности React Router 5

В React Router 5 было представлено 4 новых хука для работы с маршрутизацией.

<Route path="/">
<Home />
</Route>

useHistory

  • Предоставляет доступ к пропу history.
  • Ссылается на пакет-зависимость history, используемую роутером.
  • Основное назначение состоит в программной маршрутизации с помощью таких методов, как push, replace и др.
import { useHistory } from 'react-router-dom'

function Home() {
const history = useHistory()

return <button onClick={() => history.push('/profile')}>Профиль</button>
}

useLocation

  • Предоставляет доступ к пропу location.
  • Похож на window.location, но доступен в любом месте, поскольку является представлением состояния и текущей локации роутера.
  • Основное назначение состоит в доступе к параметрам строки запроса (query params) и полному адресу страницы.
import { useLocation } from 'react-router-dom'

function Profile() {
const location = useLocation()

useEffect(() => {
const currentPath = location.pathname
const searchParams = new URLSearchParams(location.search)
}, [location])
}

useParams

  • Предоставляет доступ к параметрам поисковой строки в URL.
  • Раньше это было возможно только с помощью match.params.
import { useParams, Route } from 'react-router-dom'

function Profile() {
const { name } = useParams()

return <p>Профиль пользователя {`${name[0].toUpperCase()}${name.slice(1)}`}</p>
}

function Dashboard() {
return (
<>
<nav>
<Link to={`/profile/иван`}>Профиль Ивана</Link>
</nav>
<main>
<Route path="/profile/:name">
<Profile />
</Route>
</main>
</>
)
}

useRouteMatch

  • Предоставляет доступ к объекту match.
  • Если используется без аргументов, возвращает ближайшее совпадение в компоненте или его предках.
  • Основное назначение состоит в создании вложенных маршрутов.
import { useRouteMatch, Route } from 'react-router-dom'

function Auth() {
const match = useRouteMatch()

return (
<>
<Route path={`${match.url}/login`}>
<Login />
</Route>
<Route path={`${match.url}/register`}>
<Register />
</Route>
</>
)
}

useRouteMatch() также может использоваться для поиска совпадения без рендеринга маршрута. Для этого ему необходимо передать аргумент location.

Компонент Redirect

Лучшим вариантом его использование является создание соответствующего свойства в состоянии компонента.

import { Redirect } from "react-router-dom"

state = { redirect: null }
render() {
if (this.state.redirect) {
return <Redirect to={this.state.redirect} />
}
}

Проп history

Каждый компонент, который является прямым потомком компонента <Route>, получает объект истории как проп. Это та же самая библиотека history, которая содержит историю сессии React Router. Мы можем использовать его свойства для навигации по определенным маршрутам.

this.props.history.push("/first")

Какого рода информация контролируется сегментом?

Сегмент (segment) контролирует 2 вида информации:

  • state: информация, которая может изменяться со временем;
  • props: пропы, передаваемые родительским компонентом дочернему, существуют в неименном виде на протяжении всего жизненного цикла потомка.

В чем заключаются недостатки паттерна MVW?

MVW расшифровывается как Model-View-Whatever (Модель-Представление-Что угодно)

  • MVC - Model-View-Controller
  • MVP - Model-View-Presenter
  • MVVM - Model-View-ViewModel
  • MVW / MV_ / MVx - Model-View-Whatever
  • HMVC - Hierarchical Model-View-Controller
  • MMV - Multiuse Model View
  • MVA - Model-View-Adapter

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

  1. Модели/контроллеры взаимодействуют друг с другом (возможно, через сервисный слой). Эти модули меняют состояния друг друга. Чем больше модулей, тем легче утратить контроль над тем, что изменило состояние того или иного модуля.
  2. Асинхронные сетевые запросы привносят элемент неожиданности в то, когда модель будет модифицирована. Представьте, что пользователь изменил UI до того, как пришел ответ на асинхронный запрос. Это приведет к "недетерминированному" статусу UI.
  3. Изменение состояния/модели добавляет еще один уровень сложности - мутацию. Требуется определить, каким образом изменяется состояние или модель, и какие инструменты необходимы для распознавания мутации.
  4. Представьте, каким сложным будет код совместно используемого приложения (такого как Google Docs, например), где множество данных меняется в режиме реального времени.
  5. Нет возможности отменять действия (возвращаться назад во времени) без добавления большого количества дополнительного кода (boilerplate).

В чем разница между React.createElement() и React.cloneElement()?

JSX-элементы транспилируются в функции React.createElement для создания React-элементов, которые используются в качестве объектного представления UI. cloneElement(), в свою очередь, используется для копирования элемента и передачи ему новых пропов.

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

import React from 'react'

export default class App extends React.Component {
// рендеринг родительского и дочернего компонентов
render() {
return (
<ParentComp>
<MyButton/>
<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-элемент.

<ReactCSSTransitionGroup
component="div"
transitionName="example"
transitionEnterTimeout={500}
transitionLeaveTimeout={500}
>
{React.cloneElement(this.props.children, {
key: this.props.location.pathname
})}
</ReactCSSTransitionGroup>

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

class Users extends React.Component {
render() {
return (
<div>
<h2>Пользователи</h2>
{this.props.children}
</div>
)
}
}

Что такое списки?

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

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

Рендеринг массива объектов в виде списка:

import React, { Component } from "react"

class Item extends Component {
state = {
items: [
{
id: 0,
context: "Успех",
modifier: "list-group-item list-group-item-success"
},
{
id: 1,
context: "Предупреждение",
modifier: "list-group-item list-group-item-warning"
},
{
id: 2,
context: "Опасность",
modifier: "list-group-item list-group-item-danger"
}
]
}

render() {
return (
<React.Fragment>
<ul className = "list-group">
{this.state.items.map((item) => (
<li key={item.id} className={item.modifier}>
{item.context}
</li>
))}
</ul>
</React.Fragment>
)
}
}

export default Item

Почему название компонента должно начинаться с большой буквы?

В JSX имена тегов в нижнем регистре считаются названиями HTML-тегов. Существует одно исключение: имена с точкой (аксессор свойства).

Когда название элемента начинается с маленькой буквы, это указывает на встроенный элемент, такой как <div> или <span>, передаваемые в React.createElement(). Названия элементов, начинающиеся с большой буквы, указывают на созданный (пользовательский) или импортированный компонент.

  • <component /> компилируется в React.createElement('component') (HTML-тег)
  • <Component /> компилируется в React.createElement(Component)
  • <obj.component /> компилируется в React.createElement(obj.component)

Что такое фрагменты? Чем они лучше div?

Фрагменты (fragments) позволяют рендерить несколько дочерних элементов за раз без добавления лишних узлов DOM:

class App extends React.Component {
render() {
return (
<React.Fragment>
<ChildA />
<ChildB />
<ChildC />
</React.Fragment>
)
}
}

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

  • Это немного повышает производительности и уменьшает расход памяти. Реальная польза от этого ощущается в очень больших и/или глубоких деревьях компонентов, однако производительность приложения зависит от тысячи мелких деталей, и это одна из них.
  • Некоторые механизмы CSS основаны на взаимоотношениях предок-потомок, добавление лишних контейнеров в середину может "поломать" макет страницы.
  • Легче инспектировать DOM с помощью DevTools.

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

Перенаправление или передача ссылки (ref forwarding) - это техника, заключающаяся в передаче ref через компонент в один из его потомков. Данная техника часто используется в библиотеках компонентов и компонентах высшего порядка (HOC).

Мы можем передать ref в компонент с помощью функции React.forwardRef(). Перенаправление ссылки позволяет компоненту передавать полученную ссылку потомку.

// Ref.js
const TextInput = React.forwardRef((props, ref) => (
<input type="text" placeholder="Привет, народ!" ref={ref} />
))

const inputRef = React.createRef()

class CustomTextInput extends React.Component {
handleSubmit = (e) => {
e.preventDefault()
console.log(inputRef.current.value)
}

render() {
return (
<div>
<form onSubmit={(e) => this.handleSubmit(e)}>
<TextInput ref={inputRef} />
<button>Отправить</button>
</form>
</div>
)
}
}

Здесь у нас имеется компонент TextInput с потомком, представляющим собой поле для ввода текста. Мы начинаем с создания ссылки:

const inputRef = React.createRef()

Затем мы передаем ссылку в <TextInput ref={inputRef}>, определяя ее как JSX-атрибут. React передает ref в функцию forwardRef() в качестве второго аргумента. Далее этот аргумент передается в <input ref={ref}>. После этого значение DOM-узла становится доступным через inputRef.current.

Обратите внимание: аналогичный функционал предоставляется хуком useRef.

Что лучше использовать, колбек-рефы или findDOMNode()?

Лучше использовать колбек-рефы (callback refs) вместо findDOMNode(). Это объясняется тем, что findDOMNode() препятствует будущим улучшениям React.

Типичный пример использования findDOMNode():

class MyComponent extends Component {
componentDidMount() {
findDOMNode(this).scrollIntoView()
}

render() {
return <div />
}
}

Рекомендуемый подход:

class MyComponent extends Component {
componentDidMount() {
this.node.scrollIntoView()
}

render() {
return <div ref={(node) => (this.node = node)} />
}
}

Чем React Router отличается от обычной маршрутизации?

React Router

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

Как React это делает?

Router "заглядывает" в History каждого компонента и при наличии изменений в истории, компонент перерисовывается. До React Router 4 нам приходилось устанавливать значение History вручную. Однако, начиная с React Router 4, большая часть работы, связанной с маршрутизацией, автоматически выполняется компонентом <BrowserRouter> (на стороне клиента).

Какие существуют способы стилизации компонентов?

  1. Таблица стилей CSS.
.DottedBox {
margin: 40px;
border: 5px dotted pink;
}

.DottedBox_content {
font-size: 15px;
text-align: center;
}
import React from 'react'
import './DottedBox.css'

const DottedBox = () => (
<div className="DottedBox">
<p className="DottedBox_content">Приступим к стилизации</p>
</div>
)

export default DottedBox
  1. Встроенные стили.

В React встроенные стили определяются не в виде строк, а в виде объектов, ключи которых именуются в стиле camelCase:

import React from 'react'

const divStyle = {
margin: '40px',
border: '5px solid pink'
}
const pStyle = {
fontSize: '15px',
textAlign: 'center'
}

const Box = () => (
<div style={divStyle}>
<p style={pStyle}>Приступим к стилизации</p>
</div>
)

export default Box
  • Мы можем создать переменную со стилями и затем передать ее в элемент - style={nameOfVariable}.
  • Мы также можем передавать стили напрямую - style={{ color: 'pink' }}.
  1. CSS-модули.

CSS-модуль - это CSS-файл, в котором названия классов и анимации имеют ограниченную (локальную) область видимости по умолчанию:

.container {
margin: 40px;
border: 5px dashed pink;
}
.content {
font-size: 15px;
text-align: center;
}
import React from 'react'
import styles from './DashedBox.css'

const DashedBox = () => (
<div className={styles.container}>
<p className={styles.content}>Приступим к стилизации</p>
</div>
)

export default DashedBox

Мы импортируем CSS-файл - import styles './DashedBox.css', затем получаем доступ к названию класса как к свойству объект -styles.className.

  1. Стилизованные компоненты.

Стилизованные компоненты (styled components) - это библиотека для React и React Native, позволяющая использовать стили на уровне компонента и смешивать CSS и JavaScript. Данная техника называется "CSS-in-JS":

yarn add styled-components
# или
npm i styled-components
import React from 'react'
import styled from 'styled-components'

const Div = styled.div`
margin: 40px;
border: 5px outset pink;

&:hover {
background-color: yellow;
}
`

const Paragraph = styled.p`
font-size: 15px;
text-align: center;
`

const OutsetBox = () => (
<Div>
<Paragraph>Приступим к стилизации</Paragraph>
</Div>
)

export default OutsetBox

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

JSX - это расширение синтаксиса JavaScript, облегчающее создание компонентов. JSX включает в себя разметку и выражения JavaScript. В действительности, JSX является сокращением для React.createElement(), делая код чище и проще.

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

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

ReactCSSTransitionGroup

ReactCSSTransitionGroup - это высокоуровневое API, основанное на ReactTransitionGroup, предоставляющее легкий способ выполнения CSS-переходов и анимации при монтировании компонента в DOM и удалении из него. Он содержит 4 компонента, отображающих переходы компонента из одного состояния в другое с помощью декларативного API:

  1. Transition
  2. CSSTransition
  3. SwitchTransition
  4. TransitionGroup
import ReactCSSTransitionGroup from 'react-transition-group'

class TodoList extends React.Component {
constructor(props) {
super(props)
this.state = { items: ['prima', 'altera', 'triera'] }
this.handleAdd = this.handleAdd.bind(this)
}

handleAdd() {
const newItems = this.state.items.concat([
prompt('Введите текст')
])
this.setState({ items: newItems })
}

handleRemove(i) {
const newItems = this.state.items.slice()
newItems.splice(i, 1)
this.setState({ items: newItems })
}

render() {
const items = this.state.items.map((item, i) => (
<div key={item} onClick={() => this.handleRemove(i)}>
{item}
</div>
))

return (
<div>
<button onClick={this.handleAdd}>Добавить элемент</button>
<ReactCSSTransitionGroup
transitionName="example"
transitionEnterTimeout={500}
transitionLeaveTimeout={300}
>
{items}
</ReactCSSTransitionGroup>
</div>
)
}
}

Здесь при добавлении нового элемента в ReactCSSTransitionGroup, ему сначала будет присвоен CSS-класс example-enter, затем example-enter-active. Это является соглашением, основанным на пропе transitionName.

Что такое синтетические события?

В React обработчики событий оборачиваются в объект SyntheticEvent. Данные объекты являются общими, т.е. объекты, получаемые обработчиками, повторно используются другими обработчиками для повышения производительности. Это также означает, что получить доступ к свойствам такого объекта не получится, поскольку они сбрасываются перед повторным использованием.

В следующем примере в консоль будет выведено null, поскольку свойства события были сброшены в SyntheticEvent:

function handleClick(event) {
setTimeout(function () {
console.log(event.target.name)
}, 1000)
}

Эту проблему можно решить посредством сохранения свойства события:

function handleClick(event) {
const name = event.target.name
setTimeout(function () {
console.log(name)
}, 1000)
}

SyntheticEvent

void preventDefault()
void stopPropagation()
boolean isPropagationStopped()
boolean isDefaultPrevented()
void persist()
boolean bubbles
boolean cancelable
DOMEventTarget currentTarget
boolean defaultPrevented
number eventPhase
boolean isTrusted
DOMEvent nativeEvent
DOMEventTarget target
number timeStamp
string type

Что такое объединение событий?

SyntheticEvent объединяются (это называется "event polling"). Объект события переиспользуется, а его свойства сбрасываются после вызова колбека обработчика. Это связано с повышением производительности. Поэтому асинхронный доступ к событию невозможен.

function onClick(event) {
console.log(event) // => null
console.log(event.type) // => "click"
const eventType = event.type // => "click"

setTimeout(function() {
console.log(event.type) // => null
console.log(eventType) // => "click"
}, 0)

// не работает: `this.state.clickEvent` имеет значение `null`
this.setState({ clickEvent: event })

// однако мы по-прежнему можем экспортировать свойства события
this.setState({ eventType: event.type })
}

Если нам требуется асинхронный доступ к свойствам события, мы можем вызвать event.persist() на событии, что удалит синтетическую событие из "пула" и позволит ссылкам на событие сохраняться в пользовательском коде.

Что такое предохранители?

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

Классовый компонент становится предохранителем, когда в нем определяются методы жизненного цикла static getDerivedStateFromError или componentDidCatch (один или оба). static getDerivedStateFromError() используется для рендеринга резервного UI после возникновения ошибки. componentDidCatch() используется для обработки ошибки.

import React, { Component } from 'react'

class ErrorBoundary extends Component {
state = {
isErrorOccured: false,
errorMessage: ''
}
componentDidCatch = (error, info) => {
this.setState({
isErrorOccured: true,
errorMessage: error.message
})
}
render() {
if(this.state.isErrorOccured) {
return <p>Ошибка: {this.state.errorMessage}</p>
} else {
return <div>{this.props.children}</div>
}
}
}

export default ErrorBoundary

Здесь у нас имеется объект состояния с двумя свойствами - isErrorOccured и errorMessage, которые будут обновлены при возникновении ошибки. Для обновления состояния мы используем метод componentDidCatch, принимающий два аргумента - error и info.

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

<ErrorBoundary>
<User/>
</ErrorBoundary>

Предохранители не перехватывают ошибки в:

  • обработчиках событий;
  • асинхронном коде (например, в setTimeout());
  • при рендеринге на стороне сервера;
  • при возникновении ошибки в самом предохранителе.

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

React-компоненты автоматически перерисовываются при изменении их состояния или пропов.

Во многих случаях метод render полагается на другие данные. В такой ситуации после монтирования компонента, сразу же выполняется его повторный рендеринг.

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

В следующем примере setState() вызывается при вводе любого символа в соответствующее поле. Это приводит к повторному рендерингу компонента и обновляет текст, отображаемый на экране:

import React, { Component } from 'react'
import 'bootstrap/dist/css/bootstrap.css'

class Greeting extends Component {
state = {
fullName: '',
}

stateChange = ({ target }) => {
const { name, value } = target
this.setState({
[name]: value,
})
}

render() {
return (
<div className="text-center">
<label htmlFor="fullName"> Полное имя: </label>
<input type="text" name="fullName" onChange={this.stateChange} />
<div className="border border-primary py-3">
<h4> Привет, {this.state.fullName}!</h4>
</div>
</div>
)
}
}

export default Greeting
  1. Использование forceUpdate()

В следующем примере при монтировании компонента генерируется случайное число. При нажатии кнопки вызывается функция forceUpdate(), что приводит к генерации нового случайного числа:

import React, { Component } from 'react'

class App extends React.Component{
constructor() {
super()
this.forceUpdateHandler = this.forceUpdateHandler.bind(this)
}

forceUpdateHandler() {
this.forceUpdate()
}

render() {
return (
<div>
<button onClick={this.forceUpdateHandler}>Принудительное обновление</button>
<h4>Случайное число: { Math.random() }</h4>
</div>
)
}
}

export default App

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

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

componentDidMount() выполняется после первого рендеринга компонента только на стороне клиента. Это подходящее место для выполнения AJAX-запросов и обновления состояния. Данный метод также используется для интеграции с другими JavaScript-фреймворками и любыми функциями с отложенным выполнением, такими как setTimeout() или setInterval().

import React, { Component } from 'react'

class App extends Component {
constructor(props) {
super(props)
this.state = {
data: 'Иван Петров'
}
}

getData(){
setTimeout(() => {
console.log('Данные получены')
this.setState({
data: 'Привет, Иван!'
})
}, 1000)
}

componentDidMount() {
this.getData()
}

render() {
return (
<div>
{this.state.data}
</div>
)
}
}

export default App

componentWillMount() выполняется перед рендерингом компонента как на стороне сервера, так и на стороне клиента. Данный метод используется для программного выполнения задач перед монтированием компонента.

import React, { Component } from 'react'

class App extends Component {
constructor(props) {
super(props)
this.state = {
data: 'Иван Петров'
}
}

componentWillMount() {
console.log('Сначала вызывается этот метод')
}

getData() {
setTimeout(() => {
console.log('Данные получены')
this.setState({
data: 'Привет, Иван!'
})
}, 1000)
}

componentDidMount() {
console.log('Затем этот')

this.getData()
}

render() {
return (
<div>
{this.state.data}
</div>
)
}
}

export default App

Для чего в React используются Webpack и Babel?

Babel

Babel - это JS-транспилятор, который преобразует новый JS-код в старый. Это очень гибкий инструмент в терминах транспиляции. Он позволяет добавлять "пресеты" (presets), такие как es2015, es2016, es2017 или env для указания JS-версии, в которую должен быть преобразован код. Babel позволяет нам иметь чистый, поддерживаемый код, в котором используются последние возможности языка без риска браузерной несовместимости.

Webpack

Webpack - это инструмент для сборки модулей, включающий 2 вида функциональности - загрузчики (loaders) и плагины (plugins). Загрузчики трансформируют исходных код модуля. Например, style-loader применяет стили к DOM с помощью тегов CSS. sass-loader компилирует SASS в CSS. babel-loader преобразует JS-код с помощью "пресетов". Плагины являются ядром Webpack. Они могут делать вещи, которые не под силу загрузчикам. Например, UglifyJS минифицирует и сжимает результат сборки.

create-react-app

create-react-app - это популярный инструмент, позволяющий создавать предварительно настроенные React-проекты с помощью одной команды. Предварительная настройка включает в себя настройку Webpack и Babel.

yarn create react-app my-app
# или
npm init react-app my-app
# или
npx create-react-app my-app

cd my-app

yarn start
# или
npm start

Почему следует избегать использования setState() после размонтирования компонента?

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

class News extends Component {
_isMounted = false // определение монтирования

constructor(props) {
super(props)

this.state = {
news: []
}
}

componentDidMount() {
this._isMounted = true

axios
.get('https://hn.algolia.com/api/v1/search?query=react')
.then((result) => {
if (this._isMounted) {
this.setState({
news: result.data.hits,
})
}
})
.catch(console.error)
}

componentWillUnmount() {
this._isMounted = false
}

render() {
return (
<ul>
{this.state.news.map((topic) => (
<li key={topic.objectID}>{topic.title}</li>
))}
</ul>
)
}
}

Здесь индикатор монтирования (_isMounted) предотвращает обновление состояния после размонтирования компонента.

Как установить фокус на поле для ввода текста после рендеринга компонента?

Ссылки (refs) могут быть полезны для доступа к узлам DOM или React-компонентам, которые возвращаются методом render. Ссылки создаются с помощью функции React.createRef и присваиваются элементу с помощью атрибута ref:

class AutoFocusTextInput extends React.Component {
constructor(props) {
super(props)
this.textInput = React.createRef()
}

componentDidMount() {
this.textInput.current.focus()
}

render() {
return <input ref={this.textInput} />
}
}

Обратите внимание: аналогичный функционал предоставляется хуком useRef.

Как реализовать таймер, обновляющийся каждую секунду?

Использование функции setInterval внутри компонента позволяет выполнять функции или другой код с определенными интервалами. Код, помещенный в интервал, выполняется до очистки (остановки) таймера. Для остановки таймера используется метод clearInterval.

class Clock extends React.Component {
constructor(props) {
super(props)
this.state = {
time: new Date().toLocaleTimeString()
}
}

componentDidMount() {
this.interval = setInterval(
() => this.tick(),
1000
)
}

componentWillUnmount() {
clearInterval(this.interval)
}

tick() {
this.setState({
time: new Date().toLocaleTimeString()
})
}

render() {
return (
<p className="App-clock">
Сейчас {this.state.time}.
</p>
)
}
}

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

Двустороннее связывание данных означает следующее:

  • Данные, которые мы изменяем в представлении, обновляют состояние.
  • Данные в состоянии обновляют представление.

Two Way Data Binding

class UserInput extends React.Component{
state = {
name: "Привет, React!"
}

handleChange = (e) =>{
this.setState({
name: e.target.value
})
}

render(){
return (
<div>
<h1>{this.state.name}</h1>
<input type="text"
onChange={this.handleChange}
value={this.state.name}
/>
</div>
)
}
}

Здесь мы регистрируем обработчик событий onChange() на поле для ввода текста, значение атрибута подключается к this.state.name, что позволяет ему синхронизироваться с данным свойством.

Ввод текста в поле запускает обработчик onChange(), который вызывает метод this.setState, обновляющий свойство this.state.name. Обновление данного свойства приводит к повторному рендерингу компонента UserInput.

Такие компоненты называются управляемыми, поскольку значение инпута контролируется React.

Двустороннее связывание данных с помощью хуков

import React, { useState } from 'react'

function App(){
const [name, setName] = useState('')

const handleChange = (e) => {
setName(e.target.value)
}

return (
<div>
<input onChange={handleChange} value={name} />
<h1>{name}</h1>
</div>
)
}

export default App

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

null

const AddToCart = ({ available }) => {
if (!available) return null

return (
<div className="full tr">
<button className="product--cart-button">Добавить в корзину</button>
</div>
)
}

Тернарный оператор

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

<div className="half">
<p>{description}</p>

{remaining === 0 ? (
<span className="product-sold-out">Товар отсутствует</span>
) : (
<span className="product-remaining">{remaining} осталось</span>
)}
</div>

В данном случае, если товаров не осталось, будет отображено "Товар отсутствует"; в противном случае, отображается количество оставшихся товаров.

Короткие вычисления

Это предполагает использование внутри JSX таких выражений, как checkIfTrue && <span>Отображается при истинном значении checkIfTrue</span>. Поскольку логический оператор "И" (&&) возвращает первый ложный операнд, выражение справа не будет оцениваться при ложном значении checkIfTrue .

<h2>
<span className="product--title__large">{nameFirst}</span>
{nameRest.length > 0 && (
<span className="product--title__small">{nameRest.join(" ")}</span>
)}
</h2>

Встроенные стили

<div style={{ display: showInfo ? "block" : "none" }}>Информация</div>

Как программно вызвать событие нажатия кнопки?

Для этого можно использовать проп ref для создания ссылки на нижележащий объект HTMLInputElement в колбеке, сохранить ссылку в поле класса и затем использовать ее для вызова метода click:

class MyComponent extends React.Component {
render() {
return (
<div onClick={this.handleClick}>
<input ref={(input) => (this.inputElement = input)} />
</div>
)
}

handleClick = (e) => {
this.inputElement.click()
}
}

Обратите внимание: стрелочная функция обеспечивает корректное лексическое окружение this в колбеке.

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

import styled from 'styled-components'

const Button = styled.button`
background: ${(props) => props.primary ? 'palevioletred' : 'white'}
color: ${(props) => props.primary ? 'white' : 'palevioletred'}
`

function MyPureComponent(props) {
return (
<div>
<Button>Обычная кнопка</Button>
<Button primary>Основная кнопка</Button>
</div>
)
}

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

import React, { useState } from "react"
import ReactDOM from "react-dom"

const toUpperCase = e => {
e.target.value = ("" + e.target.value).toUpperCase()
}

const App = () => {
const [name, setName] = useState("")

return (
<input
name={name}
onChange={e => setName(e.target.value)}
onInput={toUpperCase} // !
/>
)
}

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

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

Это не более, чем функция (propsProxyHOC), принимающая компонент (WrappedComponent) в качестве аргумента и возвращающая новый компонент.

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

const propsProxyHOC = (WrappedComponent) => {
return class extends React.Component {
render() {
const newProps = {
user: currentLoggedInUser
}

return <WrappedComponent {...this.props} {...newProps} />
}
}
}

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

  • Управление пропами.
  • Получение доступа к экземпляру через ссылки (refs).
  • Абстрагирование состояния.
  • Оборачивание/композиция компонента с другими компонентами.

Что такое инверсия наследования?

Инверсия наследования (inheritance inversion) - это HOC, который выглядит следующим образом:

const inheritanceInversionHOC = (WrappedComponent) => {
return class extends WrappedComponent {
render() {
return super.render()
}
}
}

Мы возвращаем класс, расширяющий WrappedComponent. Данная техника называется инверсией наследования, поскольку вместо расширения некоторого класса-усилителя (enhancer) с помощью WrappedComponent, последний сам пассивно расширяется. Отношения между ними напоминают инверсию.

Инверсия наследования предоставляет HOC доступ к экземпляру WrappedComponent. Это означает, что мы можем использовать state, props, методы жизненного цикла и даже метод render данного компонента.

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

  • Перехват рендеринга.
  • Управление состоянием.
class Welcome extends React.Component {
render() {
return (
<div>Добро пожаловать, {this.props.user}!</div>
)
}
}

const withUser = (WrappedComponent) => {
return class extends React.Component {
render() {
if (this.props.user) return <WrappedComponent {...this.props} />

return <div>Добро пожаловать, гость!</div>
}
}
}

const withLoaded = (WrappedComponent) => {
return class extends WrappedComponent {
render() {
const { isLoaded } = this.props

if (!isLoaded) return <div>Загрузка...</div>

return super.render()
}
}
}

export default withLoaded(withUser(Welcome))

Как установить динамический ключ для состояния?

Динамический ключ

onChange(e) {
const key = e.target.name
const value = e.target.value
this.setState({ [key]: value })
}

// или с помощью деструктуризации
onChange({ target: { name, value } }) {
this.setState({ [name]: value })
}

Вложенные состояния

handleSetState(cat, key, val) {
const category = { ...this.state[cat] }
category[key] = val
this.setState({ [cat]: category })
}

Что такое события указателя?

События указателя (pointer events) похожи на события мыши (mousedown, mouseup и т.д.), но не зависят от устройства (мышь, стилус, прикосновения и т.д.). Это позволяет избежать реализации функционала для каждого отдельного устройства.

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

В ReactDOM доступны следующие типы события:

  • onPointerDown
  • onPointerMove
  • onPointerUp
  • onPointerCancel
  • onGotPointerCapture
  • onLostPointerCapture
  • onPointerEnter
  • onPointerLeave
  • onPointerOver
  • onPointerOut

Реализация перетаскивания (Drag'n'Drop) с помощью Point Events:

import React, { Component } from 'react'
import logo from './logo.svg'
import './App.css'
import DragItem from './DragItem'

class App extends Component {
render() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h1 className="App-title">Пример реализации перетаскивания с помощью событий указателя</h1>
</header>
<div className="App-intro">
<DragItem />
</div>
</div>
)
}
}

export default App

Компонент DragItem:

import React from 'react'

const CIRCLE_DIAMETER = 100

export default class DragItem extends React.Component {
state = {
gotCapture: false,
circleLeft: 500,
circleTop: 100
}

isDragging = false
previousLeft = 0
previousTop = 0

onDown = (e) => {
this.isDragging = true
e.target.setPointerCapture(e.pointerId)
this.getDelta(e)
}

onMove = (e) => {
if (!this.isDragging) {
return
}

const { left, top } = this.getDelta(e)

this.setState(({ circleLeft, circleTop }) => ({
circleLeft: circleLeft + left,
circleTop: circleTop + top
}))
}

onUp = (e) => (this.isDragging = false)

onGotCapture = (e) => this.setState({ gotCapture: true })

onLostCapture = (e) => this.setState({ gotCapture: false })

getDelta = (e) => {
const left = e.pageX
const top = e.pageY

const delta = {
left: left - this.previousLeft,
top: top - this.previousTop,
}

this.previousLeft = left
this.previousTop = top

return delta
}

render() {
const { gotCapture, circleLeft, circleTop } = this.state

const boxStyle = {
border: '2px solid #ccc',
margin: '10px 0 20px',
minHeight: 400,
width: '100%',
position: 'relative'
}

const circleStyle = {
width: CIRCLE_DIAMETER,
height: CIRCLE_DIAMETER,
borderRadius: CIRCLE_DIAMETER / 2,
position: 'absolute',
left: circleLeft,
top: circleTop,
backgroundColor: gotCapture ? 'red' : 'green',
touchAction: 'none'
}

return (
<div style={boxStyle}>
<div
style={circleStyle}
onPointerDown={this.onDown}
onPointerMove={this.onMove}
onPointerUp={this.onUp}
onPointerCancel={this.onUp}
onGotPointerCapture={this.onGotCapture}
onLostPointerCapture={this.onLostCapture}
/>
</div>
)
}
}

Обратите внимание: это работает только в браузерах, поддерживающих спецификацию "Pointer Events".

В чем разница между обычными и "чистыми" компонентами?

PureComponent - это то же самое, что Component, за исключением автоматической реализации метода shouldComponentUpdate. Главное отличие между "чистыми" и обычными компонентами состоит в поверхностном (shallow) сравнении состояний. Поверхностное сравнение означает, что примитивы сравниваются по значению, а объекты - по ссылке. Это повышает производительность кода.

Обычно дочерний компонент рендерится вслед за родительским при изменении состояния или пропов последнего. "Чистый" компонент не следует этому правилу и остается прежним до изменения его собственных props или state.

Условия использования "чистых" компонентов

  • Состояние/пропы должны представлять собой иммутабельные объекты.
  • Состояние/пропы не должны быть иерархическими структурами.
  • При изменении данных следует вызывать forceUpdate().
// обычный классовый компонент
class App extends React.Component {
render() {
return <h1>Обычный компонент</h1>
}
}

// "чистый" классовый компонент
class Message extends React.Component {
render() {
return <h1>Чистый компонент</h1>
}
}

Как программно выполнить перенаправление на другую страницу с помощью React Router?

useHistory()

import { useHistory } from "react-router-dom"

function HomeButton() {
const history = useHistory()

function handleClick() {
history.push('/home')
}

return (
<button type="button" onClick={handleClick}>
Вернуться на главную страницу
</button>
)
}

withRouter()

import { withRouter } from 'react-router-dom'

const Button = withRouter(({ history }) => (
<button type='button' onClick={() => { history.push('/home') }}>
Вернуться на главную страницу
</button>
))

Для чего используется { …this.props } ?

Это называется spread-оператором (оператором распространения, расширения или распаковки) и позволяет легче передавать пропы.

<div {...this.props}>
Контент
</div>
const person = {
name: "Иван",
age: 30,
country: "Россия"
}

class SpreadExample extends React.Component {
render() {
const { name, age, country } = this.props

return (
<div>
<h3>Информация о пользователе:</h3>
<ul>
<li>Имя: {name}</li>
<li>Возраст: {age}</li>
<li>Страна: {country}</li>
</ul>
</div>
)
}
}

ReactDOM.render(
<SpreadExample {...person}/>,
document.getElementById('app')
)

Как передавать пропы в React Router?

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

// App.js
import React from "react"
import { render } from "react-dom"
import { Greeting } from "./components/Greeting.js"

import { BrowserRouter as Router, Route, Link } from "react-router-dom"

const styles = {
fontFamily: "sans-serif",
textAlign: "center"
}

const App = () => (
<div style={styles}>
<h2>Нажмите на ссылку ниже, чтобы перейти на другую страницу</h2>
<Link to="/greeting/народ">Перейти к /greeting/народ</Link>
</div>
)

const RouterExample = () => (
<Router>
<div>
<ul>
<li>
<Link to="/">Главная</Link>
</li>
</ul>

<hr />

<Route exact path="/" component={App} />
<Route
path="/greeting/:name"
render={(props) => <Greeting text="Привет, " {...props} />}
/>
</div>
</Router>
)

render(<RouterExample />, document.getElementById("root"))
// Greeting.js
import React from "react"

export class Greeting extends React.Component {
render() {
const { text, match: { params } } = this.props

const { name } = params

return (
<React.Fragment>
<h1>Страница приветствия</h1>
<p>
{text}{name}!
</p>
</React.Fragment>
)
}
}

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

Для этого можно воспользоваться хуком useParams:

import React from "react"
import { BrowserRouter as Router, Switch, Route, Link, useParams } from "react-router-dom"

export default function ParamsExample() {
return (
<Router>
<div>
<h2>Аккаунты</h2>
<ul>
<li>
<Link to="/netflix">Netflix</Link>
</li>
<li>
<Link to="/zillow-group">Zillow Group</Link>
</li>
<li>
<Link to="/yahoo">Yahoo</Link>
</li>
<li>
<Link to="/modus-create">Modus Create</Link>
</li>
</ul>

<Switch>
<Route path="/:id" children={<Child />} />
</Switch>
</div>
</Router>
)
}

function Child() {
// мы можем использовать хук `useParams` для доступа
// к динамическим частям `URL`
const { id } = useParams()

return (
<div>
<h3>Идентификатор: {id}</h3>
</div>
)
}

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

Для этого обычно используется метод filter.

Дочерний компонент передает родительскому идентификатор элемента, подлежащего удалению:

// Item.js
import React, { Component } from "react"

class Item extends Component {
state = {
count: this.props.item.value
}

handleIncrement = (e) => {
this.setState({ count: this.state.count + 1 })
}

render() {
return (
<React.Fragment>
<div className="card mb-2">
<h5 className={this.styleCardHeader()}>{this.getCount()}</h5>
<div className="card-body">
<button
onClick={(item) => {
this.handleIncrement({ item })
}}
className="btn btn-lg btn-outline-secondary"
>
Увеличить
</button>

<button
onClick={() => this.props.onDelete(this.props.item.id)}
className="btn btn-lg btn-outline-danger ml-4"
>
Удалить
</button>
</div>
</div>
</React.Fragment>
)
}

styleCardHeader() {
let classes = "card-header h4 text-white bg-"
classes += this.state.count === 0 ? "warning" : "primary"
return classes
}

getCount() {
const { count } = this.state
return count === 0 ? "Элементы отсутствуют" : count
}
}

export default Item

Функция handleDelete родительского компонента принимает id в качестве параметра. В данной функции используется метод filter для создания нового массива без элемента с указанным идентификатором. После этого вызывается метод setState для обновления состояния:

// Items.js
import React, { Component } from "react"
import Item from "./item"

class Items extends Component {
state = {
items: [{ id: 1, value: 0 }, { id: 2, value: 10 }, { id: 3, value: 20 }]
}

handleDelete = (itemId) => {
const items = this.state.items.filter((item) => item.id !== itemId)
this.setState({ items })
}

render() {
return (
<React.Fragment>
{this.state.items.map((item) => (
<Item
key={item.id}
onDelete={this.handleDelete}
item={item}
/>
))}
</React.Fragment>
)
}
}

export default Items

Что такое деструктуризация?

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

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

Без деструктуризации

const person = {
firstName: "Иван",
lastName: "Петров",
age: 30,
sex: ""
}

const first = person.firstName
const age = person.age
const sex = person.sex || "м"

console.log(first) // Иван
console.log(age) // 30
console.log(sex) // м -> значение по умолчанию

Деструктуризация

const person = {
firstName: "Иван",
lastName: "Петров",
age: 30,
sex: ""
}

const { firstName, age, sex = "м" } = person

console.log(firstName) // Иван
console.log(age) // 30
console.log(sex) // м -> значение по умолчанию

Деструктуризация в React

import React from 'react'
import Button from '@material-ui/core/Button'


export default function Events() {
const [counter, setCounter] = React.useState(0)

return (
<div className='Counter'>
<div>Результат: {counter}</div>
<Button
variant='contained'
color='primary'
onClick={() => setCounter(counter + 1)}
>
Увеличить
</Button>

<Button
variant='contained'
color='primary'
onClick={() => setCounter((counter > 0) ? (counter - 1) : 0)}
>
Уменьшить
</Button>
</div>
)
}

Компонент <Link> используется для перехода на новую страницу, а компонент <NavLink> используется для добавления атрибута activeClassName, который применяется для стилизации активной ссылки.

Link

<Link to="/">Главная</Link>

NavLink

<NavLink to="/" activeClassName="active">Главная</NavLink>

index.css

.active {
color: blue;
}

Routes.js

import ReactDOM from 'react-dom'
import './index.css'
import { BrowserRouter as Router, Switch, Route, NavLink } from 'react-router-dom'
import App from './App'
import Users from './users'
import Contact from './contact'
import NotFound from './notFound'

const Routes = (
<Router>
<div>
<ul>
<li>
<NavLink exact activeClassName="active" to="/">
Главная
</NavLink>
</li>
<li>
<NavLink activeClassName="active" to="/users">
Пользователи
</NavLink>
</li>
<li>
<NavLink activeClassName="active" to="/contact">
Контакты
</NavLink>
</li>
</ul>
<hr />
<Switch>
<Route exact path="/" component={App} />
<Route path="/users" component={Users} />
<Route path="/contact" component={Contact} />
<Route component={NotFound} />
</Switch>
</div>
</Router>
)

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

Что такое withRouter() в react-router-dom?

withRouter() - это компонент высшего порядка (HOC), позволяющий получать доступ к свойствам объекта history и ближайшему совпадению <Route>. withRouter() передает обновленные пропы match, location и history в оборачиваемый компонент при его рендеринге:

import React from "react"
import PropTypes from "prop-types"
import { withRouter } from "react-router"

// простой компонент, отображающий адрес текущей локации
class ShowTheLocation extends React.Component {
static propTypes = {
match: PropTypes.object.isRequired,
location: PropTypes.object.isRequired,
history: PropTypes.object.isRequired
}

render() {
const { match, location, history } = this.props

return <div>Вы находитесь по адресу {location.pathname}</div>
}
}

const ShowTheLocationWithRouter = withRouter(ShowTheLocation)

Как отобразить данные из API, полученные с помощью Axios?

axios - это основанная на промисах библиотека для отправки HTTP-запросов, которая работает как на клиенте, так и на сервере.

Возможности

  • Interceptors (перехватчики): доступ к настройкам запросов и ответов (заголовки, данные и т.д.). Эти методы могут использоваться для проверки конфигурации или добавления данных.
  • Instances (экземпляры): возможность создания переиспользуемых экземпляров с базовым URL, заголовками и другими предварительными настройками.
  • Defaults (значения по умолчанию): возможность устанавливать значения по умолчанию для часто используемых заголовков (например, Authorization) в исходящих запросах. Это может быть полезным при выполнении аутентификации на сервере при каждом запросе.

Установка

yarn add axios
# или
npm i axios

Методы

  • axios.request(config)
  • axios.get(url[, config])
  • axios.delete(url[, config])
  • axios.head(url[, config])
  • axios.options(url[, config])
  • axios.post(url[, data[, config]])
  • axios.put(url[, data[, config]])
  • axios.patch(url[, data[, config]])

Пример POST-запроса

axios.post('/url', { data: 'данные' })
.then((res)=>{
// успех
})
.catch((error)=>{
// провал
})

Пример GET-запроса

axios.get('/url')
.then((res)=>{
// успех
})
.catch((error)=>{
// провал
})

Пример одновременной отправки нескольких запросов

function getUserAccount() {
return axios.get('/user/12345')
}

function getUserPermissions() {
return axios.get('/user/12345/permissions')
}

axios.all([ getUserAccount(), getUserPermissions() ])
.then(axios.spread((account, permissions) => {
// данные из обоих запросов
}))

React

import React from 'react'
import axios from 'axios'

export default class PersonList extends React.Component {
state = {
name: '',
}

handleChange = event => {
this.setState({ name: event.target.value })
}

handleSubmit = event => {
event.preventDefault()

const user = {
name: this.state.name
}

axios.post(`https://jsonplaceholder.typicode.com/users`, { user })
.then((res) => {
console.log(res)
console.log(res.data)
})
}

render() {
return (
<div>
<form onSubmit={this.handleSubmit}>
<label>
Имя пользователя:
<input type="text" name="name" onChange={this.handleChange} />
</label>
<button type="submit">Отправить</button>
</form>
</div>
)
}
}

Как перевести приложение на другой язык с помощью react-i18next?

Установка

yarn add i18next react-i18next
# или
npm i i18next react-i18next

Настройка

Создаем файл i18n.js:

import i18n from "i18next"
import { initReactI18next } from "react-i18next"

// перевод
const resources = {
ru: {
translation: {
"welcome.title": "Добро пожаловать в React и react-i18next!"
}
}
}

i18n
.use(initReactI18next) // передаем `i18n` в `react-i18next`
.init({
resources,
lng: "ru",
keySeparator: false, // мы не используем ключи в виде `messages.welcome`
interpolation: {
escapeValue: false // `React` имеет встроенную защиту от `XSS`
}
})

export default i18n

Мы передаем экземпляр i18n в react-i18next, который делает его доступным для всех компонентов через контекст:

import React, { Component } from "react"
import ReactDOM from "react-dom"
import './i18n'
import App from './App'

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

Хуки

Функция t - основная функция i18next для перевода контента:

import React from 'react'
import { useTranslation } from 'react-i18next'

function MyComponent () {
const { t, i18n } = useTranslation()

return <h1>{t('welcome.title')}</h1>
}

HOC

Использование компонентов высшего порядка - популярный метод для расширения существующих компонентов посредством передачи им дополнительных пропов:

import React from 'react'
import { withTranslation } from 'react-i18next'

class HOC extends React.Component {
render() {
return (
<h1>{this.props.t('welcome.title')}</h1>
)
}
}

export default withTranslation()(HOC)

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

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

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

Терминология RxJS

  • Observable (наблюдаемый объект, объект, находящийся под наблюдением, издатель): поток (stream), аккумулирующий данные, которые могут передаваться через разные "треды" (threads).
  • Observer (наблюдатель, подписчик): потребляет данные, предоставляемые наблюдаемым объектом.
  • Subscription (подписка): способ получения наблюдателем данных от наблюдаемого объекта, наблюдатель должен подписаться (subscribe) на наблюдаемый объект.
  • Subject (субъект): может одновременно выступать как в роли наблюдателя, так и в роли наблюдаемого объекта. Таким образом, данные могут доставляться нескольким наблюдателям. Когда субъект получает данные, он перенаправляет их всем подписчикам.
  • Operators (операторы): методы, используемые издателями и субъектами, которые позволяют манипулировать, фильтровать и изменять издателя определенным образом. Результатом применения оператора, обычно, является новый издатель.
  • BehaviorSubject (поведенческий субъект): позволяет нескольким наблюдателям "прослушивать" (listen) поток событий, сохраняет последнее значение и передает (broadcast) его новым подписчикам.
// messageService.js
import { BehaviourSubject } from 'rxjs'

const subscriber = new BehaviourSubject(0)

const messageService = {
send: function(msg) {
subscriber.next(msg)
}
}

export {
messageService,
subscriber
}

Объект messageService отправляет функцию с аргументом msg, содержащим данные для распространения. Внутри функции вызывается метод emit, передающий данные подписчикам.

import React, { Component } from 'react'
import { render } from 'react-dom'
import './style.css'
import { subscriber, messageService } from './messageService'

class ConsumerA extends React.Component {
constructor() {
this.state = {
counter: 0
}
}

componentDidMount() {
subscriber.subscribe((v) => {
let { counter } = this.state

counter = counter + v

this.setState({ counter })
})
}

render() {
const { counter } = this.state

return (
<div>
<hr/>
<h3> Счетчик для Consumer A </h3>
<div> Значение счетчика: { counter } </div>
<hr/>
</div>
)
}
}

class ConsumerB extends React.Component {
constructor() {
this.state = {
counter: 0
}
}

componentDidMount() {
subscriber.subscribe((v) => {
let { counter } = this.state

counter = counter + v

this.setState({ counter })
})
}

render() {
const { counter } = this.state

return (
<div>
<hr/>
<h3> Счетчик для Consumer B </h3>
<div> Значение счетчика: { counter } </div>
<hr/>
<ProducerB />
</div>
)
}
}

class ProducerA extends React.Component {
render() {
return (
<div>
<h3>ProducerA</h3>
<button onClick={(e) => subscriber.next(1)}>Увеличить значение счетчика</button>
<ConsumerA />
</div>
)
}
}

class ProducerB extends React.Component {
render() {
return (
<div>
<h3>ProducerB</h3>
<button onClick={(e) => subscriber.next(-1)}>Уменьшить значение счетчика</button>
</div>
)
}
}

class App extends Component {
render() {
return (
<div>
<ProducerA />
<hr/>
<ConsumerB />
</div>
)
}
}

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

Компоненты ConsumerA и ConsumerB имеют собственные состояния для счетчиков. В методах componentDidMount они подписываются на один и тот же поток данных. При публикации события значения обоих счетчиков обновляются. Компоненты ProducerA и ProducerB содержат кнопки для увеличения и уменьшения значения счетчика. При нажатии кнопок они передают (emit) 1 или -1, соответственно.

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

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

React.lazy() позволяет создавать компоненты, загружаемые с помощью динамического import(), которые рендерятся как обычные компоненты. Это уменьшает размер "бандла" (кусок кода, отвечающий за определенную часть приложения), поскольку загружаются только те компоненты, которые отображаются на экране в данный момент.

React.lazy() принимает функцию в качестве аргумента, которая возвращает промис в результате вызова import() для загрузки компонента. Модуль "разрешается" модулем с "дефолтным" экспортом, содержащим React-компонент.

import { lazy } from 'react'

const MyComponent = lazy(() => import('./MyComponent'))

const App = () => {
<Suspense fallback={<div>Загрузка...</div>}>
<MyComponent />
</Suspense>
}

Обратите внимание: лениво загружаемый компонент должен быть обернут в компонент Suspense.

В чем заключаются преимущества использования Axios перед Fetch API для отправки HTTP-запросов?

Fetch: данный API предоставляет метод fetch, свойство глобального объекта window. Он также предоставляет интерфейс для доступа и управления HTTP-запросами и ответами. Метод fetch принимает URL запрашиваемого ресурса в качестве параметра. Данный метод возвращает промис, разрешающийся ответом на запрос:

fetch('path-to-the-resource-to-be-fetched')
.then((response) => {
// успех
})
.catch((error) => {
// провал
})

Axios: клиент-серверная библиотека для отправки HTTP-запросов, основанная на промисах. Она позволяет перехватывать запросы и обеспечивает защиту клиента от XSRF. Она также позволяет отменять запросы:

axios.get('url')
.then((response) => {
// успех
})
.catch((error) => {
// провал
})

Разница между Axios и Fetch

AxiosFetch
Имеет свойство url в объекте запросаНе имеет свойства url в объекте запроса
Является сторонней библиотекойЯвляется нативным инструментом
Имеет встроенную защиту от XSRFНе имеет встроенной защиты от XSRF
Использует свойство dataИспользует свойство body
data принимает объект с даннымиданные в body должны быть сериализованы с помощью метода JSON.stringify
Успешный запрос возвращает статус 200 и текст OKУспешный запрос возвращает свойство ok с логическим значением
Выполняет автоматическое преобразование данных в формат JSON и обратноПреобразование данных в формат JSON и обратно выполняется вручную (JSON.stringify() и response.json(), соответственно)
Позволяет отменять запросы по истечении "таймаута"Не позволяет отменять запросы
Позволяет перехватывать запросыПо умолчанию не позволяет перехватывать запросы
Имеет встроенную поддержку для индикации процесса получения данныхНе имеет встроенной поддержки для фиксации процесса получения данных

В чем разница между рендерингом и монтированием?

Рендеринг - это функция или метод render, вызываемый компонентом, возвращающие инструкции для создания DOM. Метод render вызывается при каждом рендеринге компонента. Обновление компонента происходит при изменении его state или props.

Монтирование - это первый рендеринг компонента и построение первоначальной объектной модели документа (виртуального DOM). Монтирование компонента означает встраивание создаваемых им элементов в браузерный DOM.

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

class App extends React.Component {
state = {
showUser: false
}

render() {
return (
<div>
{this.state.showUser && <User name="Иван" />}
<button onClick={() => this.setState({ showUser: true })}>
Показать пользователя
</button>
<button onClick={() => this.setState({ showUser: false })}>
Скрыть пользователя
</button>
</div>
)
}
}

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

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

Что такое Flow?

Проверка типа

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

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

  1. Статическая проверка типов.

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

  1. Динамическая проверка типов.

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

Flow

Flow - это статический типизатор для JavaScript-приложений, способный обнаруживать проблемы в коде и сообщать о них разработчику. Разработанный командой Facebook, данный типизатор перехватывает наиболее распространенные ошибки перед выполнением кода (в процессе его компиляции).

Интеграция

yarn create react-app flowchecker
# или
npm init react-app flowchecker
# или
npx create-react-app flowchecker

yarn add flow-bin
# или
npm i flow-bin

Добавляем соответствующий скрипт в файл package.json:

"scripts": {
"flow": "flow"
}

Выполняем команду для запуска Flow:

yarn flow init
# или
npm run flow init

Это приводит к созданию конфигурационного файла для Flow. В данном файле указываются проверяемые и игнорируемые файлы.

Какие существуют способы привязки обработчика событий к экземпляру?

bind() в constructor()

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

handleClick() {
console.log('Кнопка была нажата')
}

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

bind() в render()

class App extends Component {

handleClick() {
console.log('Кнопка была нажата')
}

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

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

class App extends Component {
handleClick() {
console.log('Кнопка была нажата')
}

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

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

Почему в setState() рекомендуется передавать колбек вместо объекта?

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

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

import React, { Component } from 'react'

class App extends Component {
constructor(props) {
super(props)
this.state = {
age: 0
}
}

// `this.checkAge` передается в качестве колбека в `setState()`
updateAge = (value) => {
this.setState({ age: value }, this.checkAge)
}

checkAge = () => {
const { age } = this.state

if (age >= 18) {
// успех
} else {
// провал
}
}

render() {
const { age } = this.state

return (
<div>
<p>Проверка возраста</p>
<input
type="number"
value={age}
onChange={(e) => this.updateAge(e.target.value)}
/>
</div>
)
}
}

export default App

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

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

function App() {
const [age, setAge] = useState(0)

updateAge(value) {
setAge(value)
}

useEffect(() => {
if (age >= 18) {
// успех
} else {
// провал
}
}, [age])

return (
<div>
<p>Проверка возраста</p>
<input
type="number"
value={age}
onChange={e => setAge(e.target.value)}
/>
</div>
)
}

export default App

Как определить контекст this без помощи constructor()?

Стрелочная функция одновременно создает и привязывает функцию к экземпляру. Внутри метода render контекстом функции будет экземпляр компонента:

class Button extends React.Component {
handleClick = (e) => {
console.log('Случился "клик"!')
}

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

В чем разница между ShadowDOM и VirtualDOM?

Shadow DOM

Document Object Model (объектная модель документа)

Это способ представления структуры документа с помощью объектов. Это кроссплатформенное и не зависящее от языка соглашение для представления и взаимодействия с данными в HTML, XML и т.д. Браузеры обрабатывают детали реализации DOM, поэтому мы можем взаимодействовать с ним с помощью JavaScript и CSS.

Virtual DOM (виртуальный DOM)

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

Shadow DOM (теневой DOM)

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

Разница

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

Что такое подъем состояния?

Одним из способов распределения состояния между двумя компонентами является определение состояния в их родительском компоненте. Такой подход называется подъемом состояния (state lifting). Изменения общего состояния незамедлительно отражаются на соответствующих компонентах.

Пример

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

App.js

import React from 'react'
import PlayerContent from './PlayerContent'
import PlayerDetails from './PlayerDetails'
import './App.css'

class App extends React.Component {
constructor(props) {
super(props)
this.state = { selectedPlayer: [0, 0], playerName: ''}
this.updateSelectedPlayer = this.updateSelectedPlayer.bind(this)
}

updateSelectedPlayer(id, name) {
const arr = [0, 0, 0, 0]

arr[id] = 1

this.setState({
playerName: name,
selectedPlayer: arr
})
}

render() {
return (
<div>
<PlayerContent
active={this.state.selectedPlayer[0]}
clickHandler={this.updateSelectedPlayer}
id={0}
name="Иван"
/>
<PlayerContent
active={this.state.selectedPlayer[1]}
clickHandler={this.updateSelectedPlayer}
id={1}
name="Петр"
/>
<PlayerDetails name={this.state.playerName}/>
</div>
)
}
}
export default App

PlayerContent.js

import React, { Component} from 'react'

class PlayerContent extends Component {
render () {
return (
<button
onClick={() => { this.props.clickHandler(this.props.id, this.props.name) }}
style={ {color: this.props.active ? 'red': 'blue' }}
>
{this.props.name}
</button>
)
}
}

export default PlayerContent

PlayerDetails.js

import React, { Component } from 'react'

class PlayerDetails extends Component {
render () {
return (
<div>{this.props.name}</div>
)
}
}

export default PlayerDetails

Что такое Children?

children ссылается на контейнер, содержимое которого неизвестно до передачи данных из родительского компонента. Этот контейнер позволяет передавать компоненты другим компонентам в качестве данных подобно любому другому пропу. Отличительной особенностью children является то, что React предоставляет его поддержку через ReactElement API и JSX

const Picture = (props) => {
return (
<div>
<img src={props.src} alt=""/>
{props.children}
</div>
)
}

Компонент Picture содержит элемент <img>, получает некоторые пропы и рендерит {props.children}. При рендеринге данного компонента отображается {props.children}, которая является ссылкой на любые компоненты между открывающим и закрывающим тегом Picture.

// App.js
render () {
return (
<div className='container'>
<Picture key={picture.id} src={picture.src}>
{/_ указанные здесь компоненты будут переданы как `props.children` _/}
</Picture>
</div>
)
}

Для чего предназначена команда eject в Create React App?

Интерфейс командной строки (command line interface, CLI) create-react-app позволяет создавать предварительно настроенные и оптимизированные React-проекты. Скрипт eject удаляет установленные зависимости, копирует файлы с настройками и транзитивные зависимости (такие как Webpack, Babel и др.) в файл package.json. Это означает, что перед сборкой проекта необходимо будет вручную выполнить установку зависимостей.

После выполнения eject, команды npm start и npm run build по-прежнему будут работать, но они будут обращаться к скопированным скриптам, поэтому мы можем их изменять. eject можно выполнить только один раз.

Почему использование строковых ссылок является плохой практикой?

React предоставляет ссылки на DOM-элементы, отрендеренные с помощью JSX. Раньше это выглядело так:

componentWillUpdate() {
this.refs.example.tagName == "div"
}

render() {
return (
<div ref="example"/>
)
}

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

В новых версиях React вместо строковых ссылок (string refs) используются колбеки:

render() {
return (
<div ref={(div) => { console.log('Название тега: ', div.tagName) }} />
)
}

Колбек вызывается при монтировании компонента со ссылкой на DOM-элемент в качестве аргумента. Следует отметить, что при размонтировании компонента данный колбек вызывается снова, но на этот раз с null в качестве аргумента.

Какой способ осуществления статической проверки типов является рекомендуемым?

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

В чем разница между Flow и PropTypes?

Flow - это инструмент статического анализа, использующий надмножество JavaScript, позволяющий добавлять аннотации типов и перехватывать распространенные ошибки во время компиляции кода.

PropTypes - это стандартный типизатор React. Раньше он был встроенным, сейчас находится в библиотеке prop-types. Он проверяет только пропы, передаваемые компоненту.

Что такое распределенный компонент?

Распределенный компонент (shared component) - это разновидность компонента, который управляет своим внутренним состоянием, а логику рендеринга делегирует другому компоненту. Таким образом, место определения компонента отделяется от места его реализации. Это предоставляет возможность защитить специфическую логику от остального приложения, предоставляя "чистый" и выразительный API для потребления (consume) компонентом.

Распределенные компоненты конструируются таким образом, чтобы оперировать набором данных, которые передаются через дочерние компоненты вместо пропов. Под капотом они используются низкоуровневое API, такое как React.children.map() и React.cloneElement(). С помощью этих методов компонент получает возможность к "самовыражению" способом, обеспечивающим возможность применения паттернов, связанных с композицией и масштабируемостью.

function App() {
return (
<Menu>
<MenuButton>
Операции <span aria-hidden></span>
</MenuButton>
<MenuList>
<MenuItem onSelect={() => alert('Download')}>Скачать</MenuItem>
<MenuItem onSelect={() => alert('Copy')}>Копировать</MenuItem>
<MenuItem onSelect={() => alert('Delete')}>Удалить</MenuItem>
</MenuList>
</Menu>
)
}

Компонент <Menu> содержит явно определенное совместное состояние. Компоненты <MenuButton>, <MenuList> и <MenuItem> имеют доступ к этому состоянию, все манипуляции осуществляются в явном виде. Это позволяет получить выразительный API.

Что такое React Fiber?

React Fiber (волокно) - это новый алгоритм согласования. Согласование (reconcilation) - это процесс сравнения старого и нового деревьев элементов для определения различий между ними. В старом алгоритме (который теперь называют стековым согласованием - stack reconciler) сравнение проводилось синхронно, поэтому основной поток выполнения программы был недоступен для других частей UI, таких как анимация, формирование макета страницы и обработка жестов. Fiber Reconciler преследует такие цели, как:

  • Возможность разделения операций на части.
  • Возможность задавать приоритеты, перемещать и повторно запускать операции в ходе их выполнения.
  • Возможность переключаться между предками и потомками в интересах "макетирования".
  • Возможность возвращать несколько React-элементов из ReactDOM.render().

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

React Fiber выполняет согласование в два этапа: render и commit.

  1. Методы жизненного цикла, вызываемые на стадии рендеринга (render):
  • UNSAFE_componentWillMount()
  • UNSAFE_componentWillReceiveProps()
  • getDerivedStateFromProps()
  • shouldComponentUpdate()
  • UNSAFE_componentWillUpdate()
  • render()
  1. Методы жизненного цикла, вызываемые на стадии фиксации (commit):
  • getSnapshotBeforeUpdate()
  • componentDidMount()
  • componentDidUpdate()
  • componentWillUnmount()

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

React Fiber использует requestIdleCallback() для планирования операций с низким приоритетом (так было раньше, в настоящее время вместо requestIdleCallback() используется пакет scheduler) и requestAnimationFrame() для планирования операций с высоким приоритетом.

Проблемы текущей реализации:

  • Продолжительные задачи могут приводить к пропуску кадров (frame drops).
  • Разные задачи имеют разный приоритет.

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

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

Волокно доступно для использования, но запускается в режиме совместимости с текущей реализацией.

Объясните композицию и наследование в React

Наследование

Наследование (inheritance) - это концепция объектно-ориентированного программирования, когда один класс наследует поля и методы другого класса. Это может быть полезным для обеспечения возможности повторного использования кода:

class UserNameForm extends React.Component {
render() {
return (
<div>
<input type="text" />
</div>
)
}
}

class CreateUserName extends UserNameForm {
render() {
const parent = super.render()
return (
<div>
{parent}
<button>Создать</button>
</div>
)
}
}

class UpdateUserName extends UserNameForm {
render() {
const parent = super.render()
return (
<div>
{parent}
<button>Обновить</button>
</div>
)
}
}

ReactDOM.render(
(<div>
<CreateUserName />
<UpdateUserName />
</div>),
document.getElementById('root')
)

Мы расширяем компонент UserNameForm и извлекаем его методы в дочернем компоненте с помощью super.render().

Композиция

Композиция (composition) - это также концепция ООП. Однако, вместо наследования свойств базового класса, "расширяемый" класс ссылается на экземпляры другого класса:

class UserNameForm extends React.Component {
render() {
return (
<div>
<input type="text" />
</div>
)
}
}

class CreateUserName extends React.Component {
render() {
return (
<div>
<UserNameForm />
<button>Создать</button>
</div>
)
}
}

class UpdateUserName extends React.Component {
render() {
return (
<div>
<UserNameForm />
<button>Обновить</button>
</div>
)
}
}

ReactDOM.render(
(<div>
<CreateUserName />
<UpdateUserName />
</div>),
document.getElementById('root')
)

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

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

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

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

Что такое веб-хуки?

Веб-хуки (web hooks) - это механизм, позволяющий серверу отправлять клиенту уведомления о возникновении интересующего его события. Данный механизм напоминает Server-Sent Events.

Веб-хуки иногда называют реверсивными API. В обычных API клиент обращается к серверу (потребляет его данные). В веб-хуках сервер обращается к веб-хуку (конечной точке - endpoint, предоставленной клиентом), т.е. сервер обращается к клиенту.

Что такое useCallback(), useMemo(), useImperativeHandle(), useLayoutEffect() и useDebugValue()?

useCallback()

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

function App() {
const memoizedHandleClick = useCallback(
() => console.log('Случился "клик"!'),
[]
) // сохраняем колбек без зависимостей, т.е. делаем его статичным

return <Button onClick={memoizedHandleClick}>Нажми на меня</Button>
}

useMemo()

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

Пример приложения, которое рендерит список пользователей и позволяет фильтровать пользователей по именам (фильтрация осуществляется при нажатии соответствующей кнопки):

import React from 'react'

const users = [
{ id: 'a', name: 'Иван' },
{ id: 'b', name: 'Петр' },
]

const App = () => {
const [text, setText] = React.useState('')
const [search, setSearch] = React.useState('')

const handleText = (event) => {
setText(event.target.value)
}

const handleSearch = () => {
setSearch(text)
}

// хук `useMemo`
const filteredUsers = React.useMemo(
() =>
users.filter((user) => {
console.log('Запущена функция фильтрации...')

return user.name.toLowerCase().includes(search.toLowerCase())
}),
[search]
)

return (
<div>
<input type="text" value={text} onChange={handleText} />
<button type="button" onClick={handleSearch}>
Поиск
</button>

<List list={filteredUsers} />
</div>
)
}

const List = ({ list }) => {
return (
<ul>
{list.map((item) => (
<ListItem key={item.id} item={item} />
))}
</ul>
)
}

const ListItem = ({ item }) => {
return <li>{item.name}</li>
}

export default App

filteredUsers повторно вычисляется только при изменении состояния search. Она не вызывается при изменении состояния text, поскольку данная переменная не указана в массиве зависимостей useMemo().

useImperativeHandle()

Хук useImperativeHandle позволяет кастомизировать значение, передаваемое родительскому компоненту с помощью ref. Имейте ввиду, что императивный код, в котором используются ссылки, является плохой практикой. useImperativeHandle() должен использоваться совместно с forwardRef():

function FancyInput(props, ref) {
const inputRef = useRef()

useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus()
}
}))

return <input ref={inputRef} />
}

FancyInput = forwardRef(FancyInput)

useLayoutEffect()

useLayoutEffect

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

Данный хук предназначен для выполнения тех же задач, которые выполняют методы componentDidMount и componentDidUpdate.

import React, { useState, useLayoutEffect } from 'react'
import ReactDOM from 'react-dom'

const BlinkyRender = () => {
const [value, setValue] = useState(0)

useLayoutEffect(() => {
if (value === 0) {
setValue(10 + Math.random() _ 200)
}
}, [value])

console.log('Рендеринг ', value)

return (
<div onClick={() => setValue(0)}>
Значение: {value}
</div>
)
}

ReactDOM.render(<BlinkyRender />, document.querySelector('#root'))

useLayoutEffect() против useEffect()

  • useLayoutEffect: нам нужно внести незаметные для пользователя изменения в DOM.
  • useEffect: нам не нужно взаимодействовать с DOM, или наши манипуляции с ним незаметны для пользователя (в большинстве случаев так и есть).

useDebugValue()

Хук useDebugValue может использоваться для отображения подписей к пользовательским хукам в React DevTools.

function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null)

// ...

// добавляем хуку подпись, отображаемую в инструментах разработчика
// это может выглядеть как `FriendStatus: В сети`
useDebugValue(isOnline ? 'В сети' : 'Не в сети')

return isOnline
}

Как работают перехватчики в Axios?

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

Существует 2 вида перехватчиков:

  • Перехватчик запроса: вызывается перед отправкой запроса.
  • Перехватчик ответа: вызывается перед выполнением промиса и получением данных в then().

Перехватчик запроса

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

// добавляем обработчик запроса
const requestHandler = (request) => {
if (isHandlerEnabled(request)) {
// модифицируем запрос
request.headers['X-Auth'] = 'https://example.com/qwerty'
}

return request
}

// включаем перехватчик запроса
axiosInstance.interceptors.request.use(
(request) => requestHandler(request)
)

Перехватчики ответов и ошибок

// добавляем обработчики ответа
const errorHandler = (error) => {
if (isHandlerEnabled(error.config)) {
// обрабатываем ошибки
}

return Promise.reject({ ...error })
}

const successHandler = (response) => {
if (isHandlerEnabled(response.config)) {
// обрабатываем ответ
}

return response
}

// включаем перехватчики
axiosInstance.interceptors.response.use(
(response) => successHandler(response),
(error) => errorHandler(error)
)

Как использовать useSpring() для реализации анимации?

React Spring - это основанная на физике упругости (spring - пружина) библиотека для анимации, которая поддерживает множество анимаций, связанных с UI. Это мост между двумя анимационными библиотеками для React - React Motion и Animated. Она облегчает использование react-motion.

react-spring предоставляет в распоряжение разработчика 5 хуков:

  • useSpring(): перемещение данных из точки "а" в точку "б".
  • useSprings(): несколько "пружин", для списков, где каждая "пружина" перемещает данные из точки "а" в точку "б".
  • useTrail(): несколько "пружин" с общим набором данных, одна "пружина" следует за другими.
  • useTransition(): для перехода монтирование/размонтирование (списков, в которые добавляются, обновляются и удаляются элементы).
  • useChain(): для выстраивания в цепочку (объединения) нескольких анимаций.

useSpring()

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

import { useSpring, animated } from 'react-spring'

function App() {
const props = useSpring({ opacity: 1, from: { opacity: 0 } })

return <animated.div style={props}>Я исчезаю</animated.div>
}

useSprings()

Работает как сочетание useSpring() и useTransition(), принимает массив, перебирает его элементы и использует свойства from и to для применения анимации. Для стилизации мы просто передаем в useSprings() длину массива и значение из каждого его элемента:

import React, { useState } from 'react'
import { animated, useSprings } from 'react-spring'


const App = () => {
const [on, toggle] = useState(false)

const items = [
{ color: 'red', opacity: .5 },
{ color: 'blue', opacity: 1 },
{ color: 'green', opacity: .2 },
{ color: 'orange', opacity: .8 },
]

const springs = useSprings(items.length, items.map((item) => ({
from: { color: '#fff', opacity: 0 },
to: {
color: on ? item.color : '#fff',
opacity: on ? item.opacity : 0
}
})))

return (
<div>
{springs.map((animation, index) => (
<animated.div style={animation} key={index}>Привет, народ!</animated.div>
))}

<button onClick={() => toggle(!on)}>Переключить</button>
</div>
)
}

useTrail()

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

import { animated, useTrail, config } from 'react-spring'

const App = () => {
const [on, toggle] = useState(false)

const springs = useTrail(5, {
to: { opacity: on ? 1 : 0 },
config: { tension: 250 }
})

return (
<div>
{springs.map((animation, index) => (
<animated.div style={animation} key={index}>Привет, народ!</animated.div>
))}

<button onClick={() => toggle(!on)}>Переключить</button>
</div>
)
}

useTransition()

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

import React, { useState } from 'react'
import { animated, useTransition } from 'react-spring'

const [on, toggle] = useState(false)

const transition = useTransition(on, null, {
from: { opacity: 0 },
enter: { opacity: 1 },
leave: { opacity: 0 }
})

return (
<div>
{transition.map(({ item, key, props }) => item && <animated.div style={props}>Привет, народ!</animated.div>)}

<button onClick={() => toggle(!on)}>Переключить</button>
</div>
)

useChain()

useChain() позволяет устанавливать последовательность выполнения определенных ранее хуков анимации. Для предотвращения независимого выполнения анимаций используются ссылки:

import React, { useState, useRef } from 'react'
import { animated, useSpring, useTrail, useChain} from 'react-spring'


const App = () => {
const [on, toggle] = useState(false)

const springRef = useRef()
const spring = useSpring({
ref: springRef,
from: { opacity: .5 },
to: { opacity: on ? 1 : .5 },
config: { tension: 250 }
})

const trailRef = useRef()
const trail = useTrail(5, {
ref: trailRef,
from: { fontSize: '10px' },
to: { fontSize: on ? '45px' : '10px' }
})

useChain(on ? [springRef, trailRef] : [trailRef, springRef])

return (
<div>
{trail.map((animation, index) => (
<animated.h1 style={{ ...animation, ...spring }} key={index}>Привет, народ!</animated.h1>
))}

<button onClick={() => toggle(!on)}>Переключить</button>
</div>
)
}

Как реализовать кеширование в React?

В React кеширование данных может быть реализовано разными способами:

  • Локальное хранилище.
  • Хранилище Redux.
  • Сохранение данных между монтированием и размонтированием и т.д.

Сохранение полученных данных

Мемоизация - это техника, позволяющая обеспечить сохранность данных, полученных из API при выполнении запроса на ранней стадии. Сохранение результатов "дорогого" запроса экономит пользователям время загрузки. Таким образом, повышается производительность приложения и улучшается пользовательский опыт.

const cache = {}

const useFetch = (url) => {
const [status, setStatus] = useState('idle')
const [data, setData] = useState([])

useEffect(() => {
if (!url) return

const fetchData = async () => {
setStatus('Получение данных')

if (cache[url]) {
const data = cache[url]

setData(data)

setStatus('Из кеша')
} else {
const response = await fetch(url)

const data = await response.json()

cache[url] = data // записываем данные в кеш

setData(data)

setStatus('Из сети')
}
}

fetchData()
}, [url])

return { status, data }
}

Мы привязываем данные к URL. Если мы отправляем запрос на получения данных, существующих в кеше, данные возвращаются из кеша. В противном случае, мы выполняем запрос и записываем полученные данные в кеш. Это позволяет предотвратить отправку запроса на получения данных, которые запрашивались ранее.

Сохранение данных с помощью useRef()

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

const useFetch = (url) => {
const cache = useRef({})
const [status, setStatus] = useState('idle')
const [data, setData] = useState([])

useEffect(() => {
if (!url) return

const fetchData = async () => {
setStatus('Получение данных')

if (cache.current[url]) {
const data = cache.current[url]

setData(data)

setStatus('Из кеша')
} else {
const response = await fetch(url)
const data = await response.json()

cache.current[url] = data // записываем данные в кеш

setData(data)

setStatus('Из сети')
}
}

fetchData()
}, [url])

return { status, data }
}

Использование локального хранилища

const initialState = {
someState: 'test'
}

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

// получаем состояние из локального хранилища
this.state = localStorage.getItem("appState") ? JSON.parse(localStorage.getItem("appState")) : initialState
}

componentWillUnmount() {
// сохраняем состояние в локальном хранилище
localStorage.setItem('appState', JSON.stringify(this.state))
}

render() {
// ...
}
}

export default App

Сохранение данных между монтированием и размонтированием компонента

import React, { Component } from 'react'

// определяем начальное состояние
const state = { counter: 5 }

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

// извлекаем последнее состояние
this.state = state

this.onClick = this.onClick.bind(this)
}

componentWillUnmount() {
// сохраняем состояние для следующего рендеринга
state = this.state
}

onClick(e) {
e.preventDefault()

this.setState((prev) => ({ counter: prev.counter + 1 }))
}

render() {
return (
<div>
<span>{ this.state.counter }</span>
<button onClick={this.onClick}>Увеличить</button>
</div>
)
}
}

export default Counter

Тестирование в React

Объясните юнит-тестирование в React на примере Jest и Enzyme

Jest - это JavaScript-фреймворк для юнит-тестирования, используемый Facebook для сервисов тестирования React-приложений. Jest предоставляет средства для запуска тестов, проведения сравнений и создания "моков" (наборов фиктивных данных).

Jest также предоставляет возможность тестирования посредством создания снимка (snapshot) результата рендеринга компонента и его сравнения с предыдущим результатом. Тест провалится, если снимки будут отличаться.

Enzyme - это JavaScript-утилита для тестирования, которая облегчает сравнение, управление и анализ результатов рендеринга компонентов. Enzyme, разработанная Airbnb, предоставляет некоторые замечательные методы для рендеринга компонента (или нескольких компонентов), поиска элементов и взаимодействия с ними.

Пример

# для снимков рендеринга
yarn add react-test-renderer
# или
npm i react-test-renderer

# для тестирования `DOM`
yarn add enzyme
# или
npm i enzyme
{
"react": "^16.13.1",
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.5.0",
"@testing-library/user-event": "^7.2.1",
"enzyme": "3.9",
"jest": "24.5.0",
"jest-cli": "24.5.0",
"babel-jest": "24.5.0"
}
yarn create react-app counter-app
# или
npx create-react-app counter-app
// src/App.js
import React, { Component } from 'react'

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

this.state = {
count: 0,
}
}

makeIncrementer = (amount) => () =>
this.setState((prevState) => ({
count: prevState.count + amount,
}))

increment = this.makeIncrementer(1)

render() {
return (
<div>
<p>Значение счетчика: {this.state.count}</p>
<button className="increment" onClick={this.increment}>Увеличить значение счетчика</button>
</div>
)
}
}

export default App

Тестирование установки начального значения счетчика с помощью Enzyme

 // src/App.test.js
import React from 'react'
import { shallow } from 'enzyme'
import App from './App'

describe('Компонент App', () => {
it('Начальное значение счетчика равняется 0', () => {
const wrapper = shallow(<App />)

const text = wrapper.find('p').text()

expect(text).toEqual('Значение счетчика: 0')
})
})

Тестирование увеличения значения счетчика с помощью Enzyme

// src/App.test.js
describe('Компонент App', () => {
it('При нажатии кнопки значение счетчика увеличивается на 1', () => {
const wrapper = shallow(<App />)

const incrementBtn = wrapper.find('button.increment')

incrementBtn.simulate('click')

const text = wrapper.find('p').text()

expect(text).toEqual('Значение счетчика: 1')
})
})

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

Как работает метод shallow в Enzyme?

Метод shallow используется для рендеринга отдельного компонента для тестирования. Дочерние компоненты не рендерятся. В простых случаях вызываются методы компонента constructor, render и componentDidMount:

import React from "react"
import { shallow } from "enzyme"
import Enzyme from "enzyme"
import Adapter from "enzyme-adapter-react-16"

Enzyme.configure({ adapter: new Adapter() })

function Name(props) {
return <span>Добро пожаловать, {props.name}!</span>
}

function Welcome(props) {
return (
<h1>
<Name name={props.name} />
</h1>
)
}

const wrapper = shallow(<Welcome name="Иван" />)

console.log(wrapper.debug())

Когда не следует использовать поверхностный рендеринг (shallow rendering)

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

Как работает метод mount в Enzyme?

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

Это также отлично подходит для компонентов, напрямую взаимодействующих с DOM или методами жизненного цикла. В простых случаях вызываются методы компонента constructor, render и componentDidMount:

import ListItem from './ListItem'

return (
<ul className="list-items">
{items.map((item) => <ListItem key={item} item={item} />)}
</ul>
)
import React from 'react'
import { mount } from '../enzyme'
import List from './List'

describe('Тестирование списка', () => {
it('Рендеринг элементов списка', () => {
const items = ['раз', 'два', 'три']

const wrapper = mount(<List items={items} />)

// проверяем, что не так с нашим экземпляром
console.log(wrapper.debug())

// ожидаем, что объект `wrapper` будет определен
expect(wrapper.find('.list-items')).toBeDefined()
// ожидаем, что список будет содержать хотя бы один элемент
expect(wrapper.find('.item')).toHaveLength(items.length)
})
})

Как работает метод render в Enzyme?

Статический рендеринг используется для преобразования React-компонентов в статический HTML. Это возможно благодаря библиотеке Cheerio. Потомки компонента также рендерятся. Но при этом отсутствует доступ к методам жизненного цикла.

Также в случае статического рендеринга недоступны такие методы Enzyme API, как contains и debug. Тем не менее, мы получаем доступ к богатому арсеналу методов Cheerio для управления и анализа, таким как addClass и find.

import React from 'react'
import { render } from '../enzyme'

import List from './List'
import { wrap } from 'module'

describe('Тестирование списка', () => {
it('Рендеринг списка элементов', () => {
const items = ['раз', 'два', 'три']
const wrapper = render(<List items={items} />)

wrapper.addClass('foo')
// ожидаем, что объект `wrapper` будет определен
expect(wrapper.find('.list-items')).toBeDefined()
// ожидаем, что список будет содержать хотя бы один элемент
expect(wrapper.find('.item')).toHaveLength(items.length)
})
})

Для чего предназначен пакет ReactTestUtils?

ReactTestUtils используется для тестирования React-компонентов. Он может имитировать все события JavaScript, которые поддерживаются в React. Некоторые из наиболее часто используемых методов:

  • act()
  • mockComponent()
  • isElement()
  • isElementOfType()
  • isDOMComponent()
  • renderIntoDocument()
  • Simulate()

act()

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

class Counter extends React.Component {
constructor(props) {
super(props)
this.state = {count: 0}
this.handleClick = this.handleClick.bind(this)
}

componentDidMount() {
document.title = `Вы нажали на кнопку ${this.state.count} раз`
}

componentDidUpdate() {
document.title = `Вы нажали на кнопку ${this.state.count} раз`
}

handleClick() {
this.setState(state => ({
count: state.count + 1
}))
}

render() {
return (
<div>
<p>Вы нажали на кнопку {this.state.count} раз</p>
<button onClick={this.handleClick}>
Нажми на меня
</button>
</div>
)
}
}
import React from 'react'
import ReactDOM from 'react-dom'
import { act } from 'react-dom/test-utils'
import Counter from './Counter'

let container

beforeEach(() => {
container = document.createElement('div')
document.body.appendChild(container)
})

afterEach(() => {
document.body.removeChild(container)
container = null
})

it('Рендеринг и обновление счетчика', () => {
// тестируем первый рендеринг и вызов метода `componentDidMount`
act(() => {
ReactDOM.render(<Counter />, container)
})
const button = container.querySelector('button')
const label = container.querySelector('p')

expect(label.textContent).toBe('Вы нажали на кнопку 0 раз')
expect(document.title).toBe('Вы нажали на кнопку 0 раз')

// тестируем второй рендеринг и вызов метода `componentDidUpdate`
act(() => {
button.dispatchEvent(new MouseEvent('click', { bubbles: true }))
})

expect(label.textContent).toBe('Вы нажали на кнопку 1 раз')
expect(document.title).toBe('Вы нажали на кнопку 1 раз')
})

Для чего используется пакет react-test-renderer?

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

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

import React from 'react'
import renderer from 'react-test-renderer'
import App from './app.js' // тестируемый компонент

/*
* Тестирование с помощью снимков пригодно для случаев, когда `UI` обновляется не слишком часто
*
* Стандартный тест состоит в том, что когда мобильное приложение отрисовывается, мы делаем снимок `UI`,
* и затем сравниваем его со снимком, хранящимся рядом с тестом
*/
describe('Компонент App', () => {
test('Сравнение снимка', () => {
const tree = renderer.create(<App />).toJSON()

expect(tree).toMatchSnapshot()
})
}

Почему следует использовать разработку через тестирование?

Разработка через тестирование (Test-Driven Development, TDD) - это подход, когда разработчик создает продукт после написания тестов. TDD - это метод, представляющий собой подготовку к короткому циклу разработки под названием "Красный-Зеленый-Рефакторинг".

Процесс:

  1. Добавляем тест.
  2. Запускаем тесты и смотрим, проходит ли новый тест.
  3. Пишем код для прохождения теста.
  4. Запускаем тесты.
  5. Делаем рефакторинг.
  6. Повторяем процедуру.

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

  1. Проектирование перед реализацией.
  2. Предотвращение возможных ошибок.
  3. Гарантия того, что код работает, как ожидается.

Недостатки:

  1. Разработка занимает больше времени (но может сэкономить время в долгосрочной перспективе).
  2. Сложно тестировать пограничные случаи.
  3. Сложно создавать "моки", "фейки" и "заглушки".

В чем заключается преимущество использования селектора data-test перед селекторами className или id в Jest?

Разметка и стили подвержены изменениям при изменении дизайна страницы. Поэтому при привязке к нативным атрибутам придется часто переписывать тесты. Кроме того, при использовании CSS-модулей мы не можем полагаться на названия классов. По этой причине React предоставляет атрибут data-test для индикации элементов в JSX.

import React from 'react'
import './App.scss'

function App() {
return (
<div data-test='app-header'>
Привет, React!
</div>
)
}
export default App
import React from 'react'
import { shallow } from 'enzyme'
import App from './App'

describe('Компонент App', () => {
test('Заголовок', () => {
const wrapper = shallow(<App />)
const title = wrapper.find(`[data-test='app-header']`).text()

expect(title).toMatch('Привет, React!')
})

})

Redux

Обратите внимание: после появления в React хуков, в частности, useContext и useReducer, а также новых инструментов для управления состоянием типа Zustand, целесообразность использования Redux в React-приложениях вызывает большие сомнения. Однако следует иметь ввиду, что Redux используется в подавляющем большинстве существующих React-приложений, поэтому его изучение является полезным с точки зрения перспективы поддержки и развития таких проектов.

Что такое Redux?

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

Проще говоря, Redux - это инструмент для управления состоянием (state management tool). Обычно, он используется с React, но может использоваться и с любым другим фреймворком или библиотекой. В случае с Redux, состояние приложения находится в хранилище (store), к которому имеет доступ каждый компонент.

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

Имеется центральное хранилище, содержащее состояние приложения. Каждый компонент имеет доступ к этому хранилищу, так что нет необходимости передавать пропы из одного компонента в другой или в использовании контекста. Существует 3 основных строительных блока: хранилище, редукторы (reducers) и операции (actions).

Преимущества и ограничения Redux

  1. Состояние (state).

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

  1. Предсказуемость.

Redux - это "предсказуемое хранилище для состояния" (predictable store). Поскольку редукторы являются "чистыми" (pure) функциями, они всегда возвращают одинаковый результат для одного и того же состояния и операции.

  1. Поддерживаемость.

Redux предъявляет жесткие требования к управлению состоянием, что облегчает масштабирование приложения.

  1. Тестирование и отладка.

Redux облегчает тестирование и отладку кода благодаря таким инструментам, как Redux DevTools, позволяющим "путешествовать во времени", отслеживая состояние и операции.

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

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

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

Недостатки

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

Обратите внимание: разработчики Redux попытались решить названные и другие проблемы Redux в Redux Toolkit.

Назовите ключевые концепции Redux

Redux Components

  1. Операция (action).

Операция - это статическая информация о событии, инициализирующая изменение состояния. Обновление состояния в Redux всегда начинается с операции. Операции - это объекты, содержащие обязательное свойство type и опциональное свойство payload. Операции вызываются с помощью метода store.dispatch(). Операция создается с помощью "создателя операции" (action creator).

Создатели операции - функции, помогающие создавать операции. Создатель операции возвращает объект операции, который передается редуктору:

const setLoginStatus = (name, password) => (
{
type: "LOGIN",
payload: {
username: "foo",
password: "bar"
}
}
)
  1. Редуктор (reducer).

Редукторы - это "чистые" (pure) функции, принимающие текущее состояние приложения, выполняющие над ним операцию и возвращающие новое состояние. Новое состояние - объект, описывающий изменения состояния, произошедшие в ответ на вызванную операцию.

Редуктор похож на функцию reduce в JavaScript, когда результирующее значение вычисляется на основе нескольких значений после выполнения колбека:

const LoginComponent = (state = initialState, action) => {
switch (action.type) {
// данный редуктор обрабатывает операции с типом `LOGIN`
case "LOGIN":
return state.map((user) => {
if (user.username !== action.username) {
return user
}

if (user.password === action.password) {
return {
...user,
isLoggedIn: true
}
}
})
default:
return state
}
}

Комбинация нескольких редукторов: вспомогательная функция combineReducers преобразует объект с несколькими редукторами в один редуктор для передачи методу createStore.

Синтаксис

const rootReducer = combineReducer(reducer1, reducer2)
  1. Состояние (state).

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

import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import rootReducer from './reducers'
import App from './components/App'

const store = createStore(rootReducer)

render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)

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

  1. Отправка (dispatch).

Отправка - это передача операции с типом и полезной нагрузкой редуктору:

store.dispatch({ type: 'SET_USER', payload: user })
  1. Подписка (subscribe).

Подписка - это метод, используемый для подписки на хранилище:

store.subscribe()
  1. Провайдер (provider).

Провайдер - это компонент, содержащий ссылку на хранилище и передающий данные из хранилища дочерним компонентам.

  1. Подключение (connect).

connect - это функция, взаимодействующая с провайдером.

  1. Посредник (middleware).

Посредник - это способ расширения Redux дополнительным функционалом. Посредники используются для отправки асинхронных операций. Они настраиваются в момент создания хранилища.

Синтаксис

const store = createStore(reducers, initialState, middleware)

Что значит "единственный источник истины"?

Единственный источник истины (single source of truth) - это состояние, которое не перезаписывается и структура которого является постоянной. Это позволяет получать информацию за постоянное время и поддерживать четкую структуру состояния приложения.

В React/Redux-приложениях единственным способом изменить данные в UI является отправка операции в редуктор, обновляющий состояние приложения. Компоненты, подписанные на редукторы, обновляются вслед за ними. Это не относится к локальному состоянию компонентов.

Redux State

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

В чем заключаются особенности потока данных в Redux?

Использование Redux с React избавляет от необходимости поднимать состояние к ближайшему общему предку компонентов. Все обрабатывается Redux. Это упрощает приложение и облегчает его поддержку.

  • Redux предоставляет возможность хранить состояние всего приложения в одном месте - хранилище.
  • Компоненты отправляют изменения состояния в хранилище, а не в другие компоненты.
  • Компоненты, зависящие от состояния, должны быть подписаны на хранилище.
  • Хранилище - это своего рода посредник между состоянием и действиями, приводящими к его изменению.
  • Компоненты не взаимодействуют друг с другом напрямую. Все изменения проходят через хранилище.

Ключевые принципы

Redux основан на 3 принципах:

1. Единственный источник истины: состояние всего приложения содержится в одном хранилище в виде объекта. 2. Состояние является доступным только для чтения: единственный способ изменить состояние - отправить операцию, т.е. объект, описывающий произошедшее. 3. Изменения осуществляются с помощью "чистых" функций: для определения изменений состояния в ответ на операции используются редукторы.

Поток данных

Redux позволяет управлять состоянием приложения с помощью хранилища. Дочерний компонент может напрямую получать доступ к состоянию из хранилища.

Подробности работы Redux:

  • При возникновении пользовательского события (onClick, onChange и т.д.) в редуктор отправляется определенная операция.
  • Редуктор обрабатывает операцию и возвращает новое состояние в виде объекта.
  • Новое состояние приложение записывается в хранилище.
  • Компоненты, подписанные на хранилище, получают уведомление об изменении состояния и обновляются.

Redux Workflow

Для чего используется Redux Thunk?

Redux Thunk - это посредник, позволяющий вызывать создателей операции, возвращающих функцию вместо объекта. Данная функция получает метод dispatch, который используется для отправки синхронной операции в теле функции после завершения асинхронных операций. Внутренняя функция принимает методы dispatch и getState в качестве параметров.

Настройка

# создаем `React-проект`
yarn create react-app my-simple-async-app
# или
npm init react-app my-simple-async-app
# или
npx create-react-app my-simple-async-app

# переключаем директорию
cd my-simple-app

# устанавливаем `Redux` и `Redux-Thunk`
yarn add redux react-redux redux-thunk
# или
npm i redux react-redux redux-thunk

Пример

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

import { applyMiddleware, combineReducers, createStore } from 'redux'

import thunk from 'redux-thunk'

// actions.js
export const addRepos = (repos) => ({
type: 'ADD_REPOS',
repos,
})

export const clearRepos = () => ({ type: 'CLEAR_REPOS' })

export const getRepos = (username) => async (dispatch) => {
try {
const url = `https://api.github.com/users/${username}/repos?sort=updated`
const response = await fetch(url)
const body = await response.json()
dispatch(addRepos(body))
} catch (error) {
console.error(error)
dispatch(clearRepos())
}
}

// reducers.js
export const repos = (state = [], action) => {
switch (action.type) {
case 'ADD_REPOS':
return action.repos
case 'CLEAR_REPOS':
return []
default:
return state
}
}

export const reducers = combineReducers({ repos })

// store.js
export function configureStore(initialState = {}) {
const store = createStore(reducers, initialState, applyMiddleware(thunk))

return store
}

export const store = configureStore()

applyMiddleware(thunk) указывает Redux возвращать значения в виде функций. Обычно, Redux работает с объектами: { type: 'ADD_THINGS', things: ['list', 'of', 'things'] }.

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

// обычная синхронная операция
function syncAction(listOfThings) {
return { type: 'ADD_THINGS', things: listOfThings }
}

// асинхронная версия
// мы запрашиваем у сервера список репозиториев
// перед их добавлением посредством синхронной операции
function asyncAction() {
return function(dispatch) {
const timerId = setTimeout(function() {
dispatch(syncAction(['list', 'of', 'things']))
clearTimeout(timerId)
}, 1000)
}
}

App.js:

import React, { Component } from 'react'
import { connect } from 'react-redux'
import { getRepos } from './redux'

export class App extends Component {
state = { username: 'harryheman' }

componentDidMount() {
this.updateRepoList(this.state.username)
}

updateRepoList = (username) => this.props.getRepos(username)

render() {
return (
<div>
<h1>Аз есмь асинхронное приложение!!!</h1>

<strong>Имя GitHub-пользователя: </strong>
<input
type="text"
value={this.state.username}
onChange={({ target: { value } }) => this.setState({ username: value })}
placeholder="Имя..."
/>
<button onClick={() => this.updateRepoList(this.state.username)}>
Получить репозитории
</button>

<ul>
{this.props.repos.map((repo, index) => (
<li key={index}>
<a href={repo.html_url} target="_blank" rel="noopener noreferrer">
{repo.name}
</a>
</li>
))}
</ul>
</div>
)
}
}

const mapStateToProps = (state, ownProps) => ({ repos: state.repos })
const mapDispatchToProps = { getRepos }
const AppContainer = connect(mapStateToProps, mapDispatchToProps)(App)

export default AppContainer

index.js:

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

// импортируем провайдер и хранилище
import { Provider } from 'react-redux'
import { store } from './redux'

// оборачиваем существующее приложение провайдером
ReactDOM.render(
<Provider store={store}>
<AppContainer />
</Provider>,
document.getElementById('root')
)

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

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

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

Пример

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

this.state = {
images: []
}
}

componentDidMount() {
fetch('/api/current_user/image_list')
.then((response) => response.json())
.then((images) => this.setState({ images }))
}

render() {
return (
<div className="image-list">
{this.state.images.map((image) => {
<div className="image">
<img src={image.url} alt="" />
</div>
})}
</div>
)
}
}
  1. Представитель.
  • Отвечает за внешний вид (UI).
  • Содержит метод render и другую незначительную логику.
  • Не знает, как получать и изменять данные для рендеринга.
  • Обычно, является функциональным компонентом без состояния.

Пример

// классовый компонент
class Image extends Component {
render() {
return <img src={this.props.imageUrl} alt="" />
}
}
export default Image

// функциональный компонент
const Image = (props) => (
<img src={props.imageUrl} alt="" />
)
export default Image

Объясните назначение редуктора?

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

  1. Состояние (state).

Изменение состояния зависит от действий пользователя или сетевых запросов. Если состояние приложения управляется Redux, изменения происходят в редукторе - это единственное место, в котором происходит изменение состояния. Редуктор использует текущее состояние приложение и операцию для генерации нового состояния.

Синтаксис

const contactReducer = (state = initialState, action) => {
// ...
}
  1. Параметр state.

Аргумент state, передаваемый в редуктор, должен быть текущим состоянием приложения. В данном случае мы передаем редуктору initialState, т.е. начальное состояние, которому ничего не предшествовало.

contactReducer(initialState, action)

Пример

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

const initialState = {
contacts: []
}
  1. Параметр action.

action - это объект, содержащий две пары ключ/значение. Характер обновления состояния, происходящего в редукторе, зависит от значения свойства action.type.

const action = {
type: 'NEW_CONTACT',
payload: {
name: 'Иван Петров',
location: 'Москва',
email: 'ivan@mail.com'
}
}

Обычно, также имеется свойство payload, содержащее данные, отправленные пользователем, используемые для обновления состояния. Следует отметить, что action.type является обязательным, а action.payload опциональным.

  1. Обновление состояния.

Состояние должно быть иммутабельным. Это означает, что его нельзя изменять напрямую. Для обновления состояния можно использовать Object.assign() или spread-оператор.

Пример

const contactReducer = (state, action) => {
switch (action.type) {
case 'NEW_CONTACT':
return {
...state,
contacts: [...state.contacts, action.payload]
}
default:
return state
}
}

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

const initialState = {
contacts: [{
name: 'Иван Петров',
age: 30
}]
}

const contactReducer = (state = initialState, action) => {
switch (action.type) {
case "NEW_CONTACT":
return Object.assign({}, state, {
contacts: [...state.contacts, action.payload]
})
default:
return state
}
}

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

handleSubmit = (e) => {
e.preventDefault()
const action = {
type: "NEW_CONTACT",
payload: {
name: this.name.current.value,
age: this.age.current.value
}
}

const newState = contactReducer(this.state, action)

this.setState(newState)
}

render() {
const { contacts } = this.state

return (
<div className="box">
<div className="content">
<pre>{JSON.stringify(this.state, null, 2)}</pre>
</div>

<div className="field">
<form onSubmit={this.handleSubmit}>
<div className="control">
<input className="input" placeholder="Имя" type="text" ref={this.name} />
</div>
<div className="control">
<input className="input" placeholder="Возраст" type="number" ref={this.age} />
</div>
<div>
<button type="submit" className="button">Отправить</button>
</div>
</form>
</div>
</div>
)
}
}

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

Что такое "чистые" функции и почему редуктор должен быть такой функцией?

"Чистая" функция

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

Таким образом, функция является чистой, если она удовлетворяет следующим требованиям:

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

Пример

Приведенная ниже функция add не изменяет параметры a и b, не зависит от внешних данных, и всегда возвращает одинаковый результат для одних и тех же аргументов:

const add = (a, b) => a + b

Почему редуктор должен быть чистой функцией?

Redux берет текущее состояние (объект) и передает его каждому редуктору в цепочке (при комбинации редукторов). Редуктор должен вернуть новый объект в случае изменения состояния. При отсутствии изменений редуктор должен вернуть старый объект.

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

Как разделить редукторы?

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

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

  1. Небольшие вспомогательные функции, содержащие некоторую переиспользуемую логику, которая требуется в нескольких местах приложения (они могут быть, а могут и не быть связаны с определенной бизнес-логикой).
  2. Функции, предназначенные для обработки специальных случаев работы с состоянием, например, когда требуются параметры, кроме state и action.
  3. Функции для обработки всех обновлений определенного "среза" (slice) состояния. Такие функции, как правило, имеют стандартную сигнатуру редуктора.

Для дифференциации указанных функций используются следующие термины:

  • reducer: функция с сигнатурой (state, action) -> newState (функция, которая может быть передана в качестве аргумента в метод Array.prototype.reduce())
  • root reducer: редуктор, передаваемый в качестве первого аргумента в метод createStore. Данная функция также должна иметь сигнатуру (state, action) -> newState
  • slice reducer: редуктор, который используется для обработки определенного состояния. Обычно, он передается в качестве аргумента в метод combineReducers
  • case function: функция, которая используется для обработки логики обновления для специальной операции. Это может быть редуктор или функция, принимающая другие параметры.
  • higher-order reducer: функция, принимающая редуктор в качестве аргумента и возвращающая новый редуктор (к данной категории относится, например, combineReducers() или redux-undo).

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

  • Повышение скорости загрузки страницы

Разделение редукторов повышает скорость рендеринга страниц за счет загрузки только необходимого кода.

  • Организация кода

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

  • Одна страница/компонент

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

  • SEO

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

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

Тип операции

Action type - это строка, описывающая тип операции. Обычно, типы определяются в виде констант или перечислений (enums):

export const Actions = {
GET_USER_DETAILS_REQUEST: 'GET_USER_DETAILS_REQUEST',
GET_USER_DETAILS_SUCCESS: 'GET_USER_DETAILS_SUCCESS',
GET_USER_DETAILS_FAILURE: 'GET_USER_DETAILS_FAILURE',
...
}

Операция

Операция похожа на сообщение, отправляемое (dispatch) в хранилище. Теоретически, она может выглядеть как угодно. Однако, лучше придерживаться следующего соглашения (определение типа с помощью TypeScript):

type Action = {
type: string // операция ДОЛЖНА иметь тип
payload?: any // операция МОЖЕТ иметь полезную нагрузку
meta?: any // операция МОЖЕТ иметь мета (дополнительную) информацию
error?: boolean // операция МОЖЕТ иметь поле для ошибки
// при истинном значении данного поля, полезная нагрузка ДОЛЖНА включать ошибку
}

Операция получения данных пользователя по имени "Иван" может выглядеть так:

{
type: 'GET_USER_DETAILS_REQUEST',
payload: { name: 'Иван', age: 30 }
}

Создатель операции

export const getUserDetailsRequest = (id) => ({
type: Actions.GET_USER_DETAILS_REQUEST,
payload: id
})

В простых случаях создатель операции возвращает операцию. После этого она отправляется в хранилище:

store.dispatch(getUserDetailsRequest('Иван'))

На практике это, как правило, реализуется через метод dispatch, передаваемый в React-компонент:

export const mapDispatchToProps = (dispatch) => ({
onClick: () => dispatch(getUserDetailsRequest('Иван'))
})

Как выглядит поток данных в React/Redux-приложении?

Redux Data Flow

Redux предоставляет способ распределения данных между компонентами с помощью единого состояния в хранилище. Хранилище - это единственный источник истины (single source of truth). Компоненты, которым нужен доступ к состоянию, подписываются на хранилище, и получают состояние при его обновлении.

Redux зиждется на 5 столпах: создатели операции, диспетчеры, редукторы, состояние и хранилище.

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

Назовите принципы, которым следует Redux

Redux следует 3 основным принципам:

  1. Единственный источник истины (single source of truth).

Состояние всего приложения хранится в виде объекта в едином хранилище.

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

console.log(store.getState())

/_ Вывод
{
visibilityFilter: 'SHOW_ALL',
todos: [
{
id: 1,
text: 'Рассмотреть возможность использования Redux',
completed: true
},
{
id: 2,
text: 'Хранить состояние в одном месте',
completed: false
}
]
}
_/
  1. Состояние доступно только для чтения (readonly).

Единственным способом изменить состояния является отправка операции - объекта, описывающего, что произошло.

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

store.dispatch({
type: 'COMPLETE_TODO',
id: 1
})

store.dispatch({
type: 'SET_VISIBILITY_FILTER',
filter: 'SHOW_COMPLETED'
})
  1. Изменения осуществляются с помощью "чистых" функций (pure functions).

Для определения того, как меняется состояние в зависимости от операции, используются "чистые" редукторы (reducers).

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

import { combineReducers, createStore } from 'redux'

function visibilityFilter(state = 'SHOW_ALL', action) {
switch (action.type) {
case 'SET_VISIBILITY_FILTER':
return action.filter
default:
return state
}
}

function todos(state = [], action) {
switch (action.type) {
case 'ADD_TODO':
return [
...state,
{
id: nanoid(),
text: action.text,
completed: false
}
]
case 'COMPLETE_TODO':
return state.map((todo) => {
if (todo.id === action.id) {
return Object.assign({}, todo, {
completed: true
})
}
return todo
})
default:
return state
}
}

const reducer = combineReducers({ visibilityFilter, todos })
const store = createStore(reducer)

Как реализовать побочный эффект, такой как AJAX-запрос, в Redux?

Любое реальное приложение нуждается в сложной логике, обычно, включающей в себя выполнение асинхронных задач, таких как отправка AJAX-запросов. Такой код не является "чистым", взаимодействия с внешним миром именуются "побочными эффектами".

Redux вдохновлен функциональным программированием и из коробки не предоставляет возможностей для реализации побочных эффектов. Редукторы должны быть "чистыми" функциями с сигнатурой (state, action) => newState. Тем не менее, посредники (middlewares) Redux (например, Redux Thunk и Redux Saga) делают возможным перехват отправленных операций и добавление в них дополнительного поведения, включая выполнение побочных эффектов.

Что означает символ @ перед connect()?

Символ @ перед названием функции означает, что мы имеем дело с декоратором (decorator).

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

Ниже приведены примеры настройки Redux с и без помощи декоратора.

Без декоратора

import React from 'react'
import _ as actionCreators from './actionCreators'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'

function mapStateToProps(state) {
return { todos: state.todos }
}

function mapDispatchToProps(dispatch) {
return { actions: bindActionCreators(actionCreators, dispatch) }
}

class MyApp extends React.Component {
// ...
}

export default connect(mapStateToProps, mapDispatchToProps)(MyApp)

С декоратором

import React from 'react'
import _ as actionCreators from './actionCreators'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'

function mapStateToProps(state) {
return { todos: state.todos }
}

function mapDispatchToProps(dispatch) {
return { actions: bindActionCreators(actionCreators, dispatch) }
}

@connect(mapStateToProps, mapDispatchToProps)
export default class MyApp extends React.Component {
// ...
}

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

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

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

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

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

В приведенном ниже примере показано, как получить JWT-токен из хранилища.

Экспорт хранилища

import { createStore } from 'redux'
import reducer from './reducer'

const store = createStore(reducer)

export default store

Мы создаем хранилище и экспортируем его. Это делает его доступным для других файлов. Далее мы отправляем запрос к серверу, в который необходимо передать токен:

import store from './store'

export function getProtectedThing() {
// получаем текущее состояние
const state = store.getState()

// извлекаем токен из состояния
// (зависит от структуры хранилища)
const authToken = state.currentUser.token

// передаем токен серверу
return fetch('/user/thing', {
method: 'GET',
headers: {
Authorization: `Bearer ${authToken}`
}
})
.then((res) => res.json())
.catch(console.error)
}

Передача значения из компонента

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

import React from 'react'
import { connect } from 'react-redux'
import _ as api from 'api'

const ItemList = ({ authToken, items }) => {
return (
<ul>
{items.map((item) => (
<li key={item.id}>
{item.name}
<button onClick={() => api.deleteItem(item, authToken)}>
Удалить элемент
</button>
</li>
))}
</ul>
)
}

const mapStateToProps = (state) => ({
authToken: state.currentUser && state.currentUser.authToken,
items: state.items
})

export connect(mapStateToProps)(ItemList)

В чем разница между Redux и Flux?

Flux

Flux - это архитектурный паттерн проектирования, разработанный и используемый Facebook для построения пользовательских интерфейсов или клиентских веб-приложений. Во Flux используется однонаправленный поток данных (one-directional data flow), что соответствует композиционной природе React-компонентов.

Архитектура Flux

Архитектура Flux основана на следующих компонентах:

  • Store/Stores (хранилище/хранилища): служит контейнером для состояния и логики приложения.
  • Action (операция): позволяет передавать данные диспетчеру.
  • View (представление): то же самое что представление в паттерне MVC, но в контексте React-компонентов.
  • Dispatcher (диспетчер): координирует операции и обновление хранилища.

Flux Architecture

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

Архитектура Redux

Redux - это библиотека, реализующая идеи Flux, но немного иначе. Redux включает такие дополнительные компоненты, как:

  • Reducer (редуктор): "чистая" функция, определяющая характер изменения данных.
  • Centralized store (централизованное хранилище): содержит объект состояния всего приложения.

Redux Architecture

В Redux событие является операцией, которая отправляется в редуктор. Редуктор выполняет операцию над текущим состоянием и возвращает новое состояние, которое отправляется в хранилище. Хранилище создает новое состояние и отправляет его представлению. После этого представление перерисовывается в соответствии с новым состоянием.

Flux против Redux

FluxRedux
Однонаправленный поток данныхОднонаправленный поток данных
Может существовать несколько хранилищСуществует только одно хранилище
Вся логика обрабатывается хранилищемВся логика обрабатывается редуктором
Простая отладка обеспечивается диспетчеромЕдиное хранилище сильно облегчает отладку

В чем разница между Flux и MVC?

  1. MVC.

MVC расшифровывается как Model-View-Controller (Модель-Представление-Контроллер). Это архитектурный паттерн, который используется для разработки UI. Приложение делится на три логических компонента: модель, представление и контроллер, соответственно.

MVC

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

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

  • Отделение представления от модели: позволяет реализовывать разные UI и обеспечивает лучшую тестируемость.
  • Отделение контроллера от представления: используется в веб-интерфейсах и почти не используется в GUI-фреймворках.

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

  1. Flux.

Flux ставит во главу угла однонаправленный поток данных. В основе Flux лежит 4 элемента:

  • Операции (actions), которые являются вспомогательными методами для доставки информации диспетчеру.
  • Хранилища (stores), которые похожи на модели в MVC, за исключением того, что они выступают в роли контейнеров для состояния и логики приложения.
  • Диспетчер (dispatcher) принимает операции и является своего рода единым реестром колбеков всех хранилищ приложения. Он также управляет зависимостями между хранилищами.
  • Представления, которые похожи на представления в MVC, но в контексте React. Также включают в себя контроллеров для регистрации событий и извлечении состояния приложения.

Flux

  1. Все данные приложения передаются через центральный "хаб" - диспетчер.
  2. Эти данные отслеживаются как операции, которые передаются в диспетчер из метода создателя операции, часто как результат взаимодействия пользователя с представлением.
  3. Диспетчер вызывает колбек, передавая операцию всем хранилищам, зарегистрированным с помощью этого колбека.
  4. Хранилища уведомляют подписанные на них контроллеры-представления.
  5. Контроллеры-представления извлекают данные из соответствующих хранилищ и перерисовываются вместе с дочерними компонентами.

MVC против Flux

MVCFlux
Как правило, двунаправленный поток данныхОднонаправленный поток данных
Ключевым игроком является связывание данныхГлавные игроки - события или операции
Бизнес-логика обрабатывается контроллерамиВсе вычисления выполняются хранилищем
Обычно, является синхроннымМожет быть реализован как полностью асинхронный
Сложно отлаживатьЛегкая отладка благодаря общей точке инициализации - диспетчеру
Код в больших приложения тяжело поддерживатьКод в больших приложениях поддерживать относительно легко

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

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

Метод applyMiddleware принимает посредников в качестве аргументов через запятую (не в виде массива). Он представляет собой прослойку между отправкой операции и ее попаданием в редуктор. Это может использоваться для авторизации, получения отчетов об ошибках, обращению к асинхронным API, маршрутизации и т.д.

const createStoreWithMiddleware = applyMiddleware(ReduxThunk, logger)(createStore)

Пример "кастомного логгера"

import { createStore, applyMiddleware } from 'redux'
import todos from './reducers'

function logger({ getState }) {
return (next) => (action) => {
console.log('Осуществляется отправка: ', action)

// вызываем следующий метод отправки в цепочке посредников
const returnValue = next(action)

console.log('Состояние после отправки: ', getState())

// это будет самой операцией до тех пор,
// пока следующий посредник не изменит ее
return returnValue
}
}

const store = createStore(todos, ['Использовать Redux'], applyMiddleware(logger))

store.dispatch({
type: 'ADD_TODO',
text: 'Понять, что такое посредники'
})
// (результаты работы логгера:)
// Осуществляется отправка: { type: 'ADD_TODO', text: 'Понять, что такое посредники' }
// Состояние после отправки: [ 'Использовать Redux', 'Понять, что такое посредники' ]

Как установить начальное состояние?

  1. Инициализация состояния.

В Redux состояние всего приложения находится в хранилище, которое является древовидным объектом. Единственным способом изменить состояние является отправка операции.

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

Пример

Создаем хранилище:

import { createStore } from 'redux'

function todosReducer(state = [], action) {
switch (action.type) {
case 'ADD_TODO':
return state.concat([action.payload])
default:
return state
}
}

const store = createStore(todosReducer)

Обновляем состояние:

const ADD_TODO = 'ADD_TODO' // тип операции
const newTodo = "Написать статью"
function todoActionCreator(newTodo) {
const action = {
type: ADD_TODO,
payload: newTodo
}
dispatch(action)
}

После создания хранилища, Redux отправляет операцию в редуктор для записи в хранилище начального состояния.

  1. createStore().

Метод createStore может принимать опциональное значение preloadedState в качестве второго аргумента. В нашем примере мы не используем данный параметр. Когда такое значение передается, оно становится начальным состоянием хранилища.

const initialState = ["завтрак", "код", "сон"]
const store = createStore(todosReducer, initialState)
  1. Редуктор.

Редукторы также могут определять начальное состояние в случае, когда значением передаваемого им аргумента state является undefined:

function todosReducer(state = [], action) {
switch (action.type) {
case 'ADD_TODO':
return state.concat([action.payload])
default:
return state
}
}
/_
- начальное состояние равняется `[]`, которое будет использовано только
- при неопределенном значении начального состояния,
- т.е. когда оно не было передано в `createStore()`
_/

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

Существует ли что-либо общее между Redux и RxJS?

Redux

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

RxJS

RxJS - это реактивное расширение JavaScript. Это библиотека для реактивного программирования с помощью наблюдаемых объектов (observables). Она позволяет легко комбинировать асинхронный или основанный на колбеках код.

Redux относится к категории "инструментов для управления состоянием", а RxJS - к категории "фреймворков для конкурентного выполнения кода".

ReduxRxJS
Инструмент для управления состоянием приложенияБиблиотека реактивного программирования
Обычно, используется как архитектура UIКак правило, используется для выполнения асинхронных задач
Реализует реактивную парадигму, поскольку хранилище является реактивным. Хранилище "наблюдает" за операциями и обновляет себяТакже следует реактивной парадигме, но представляет собой не архитектуру, а строительные блоки - наблюдаемые объекты, позволяющие реализовать данный паттерн

Пример

import React from 'react'
import ReactDOM from 'react-dom'
import { Subject } from 'rxjs/Subject'

// создаем поток в качестве субъекта
// поток принимает произвольные данные
const action$ = new Subject()

// начальное состояние
const initState = { name: 'Иван' }

// редуктор `Redux`
const reducer = (state, action) => {
switch(action.type) {
case 'NAME_CHANGED':
return {
...state,
name: action.payload
}
default:
return state
}
}

// Reduxification
const store$ = action$
.startWith(initState)
.scan(reducer)

// функция высшего порядка для отправки операций в поток
const actionDispatcher = (func) => (...args) =>
action$.next(func(...args))

// пример операции
const changeName = actionDispatcher((payload) => ({
type: 'NAME_CHANGED',
payload
}))

// компонент представления `React`
const App = (props) => {
const { name } = props

return (
<div>
<h1>{ name }</h1>
<button onClick={() => changeName('Иван')}>Иван</button>
<button onClick={() => changeName('Петр')}>Петр</button>
</div>
)
}

// выполняем подписку и рендерим представление
const dom = document.getElementById('app')
store$.subscribe((state) =>
ReactDOM.render(<App {...state} />, dom))

Асинхронные операции

Предположим, что мы хотим сделать что-то асинхронное, например, получить данные из REST API. Для этого нужно отправить AJAX-поток в качестве полезной нагрузки операции и затем использовать метод flatMap библиотеки lodash для возвращения результатов асинхронной операции обратно в поток action$:

import { isObservable } from './utils'

// создатель операции
const actionCreator = (func) => (...args) => {
const action = func.call(null, ...args)
action$.next(action)
if (isObservable(action.payload)) action$.next(action.payload)
return action
}

// метод, вызываемый в ответ на нажатие кнопки
const loadUsers = actionCreator(() => {
return {
type: 'USERS_LOADING',
payload: Observable.ajax('/api/users')
.map(({ response }) => map(response, 'username'))
.map((users) => ({
type: 'USERS_LOADED',
payload: users
}))
}
})

// редуктор
export default function reducer(state, action) {
switch (action.type) {
case 'USERS_LOADING':
return {
...state,
isLoading: true
}
case 'USERS_LOADED':
return {
...state,
isLoading: false,
users: action.payload,
}
//...
}
}

// остальной код

// оборачиваем входные данные, чтобы работать только с потоком `observables`
const ensureObservable = (action) =>
isObservable(action)
? action
: Observable.from([action])

// используем `flatMap()` для преобразования асинхронных потоков
const action$
.flatMap(wrapActionToObservable)
.startWith(initState)
.scan(reducer)

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

Для чего предназначены константы?

  • Обеспечивают согласованность названий операций, поскольку все константы находятся в одном месте.
  • Иногда полезно взглянуть на существующие операции перед началом работы над новой "фичей". Вполне вероятно, что необходимая операция уже была добавлена другим членом команды, а мы об этом просто не знали.
  • Список добавленных, удаленных и измененных типов операций в "пулл-реквесте" помогает каждому члену команды следить за состоянием и реализацией новой "фичи".
  • Если мы допустили опечатку при определении константы, то получим undefined. Это позволяет выявить ошибку на ранней стадии - при отправке операции.

Пример

Константы в Redux используются в 2 местах - в редукторах и в создателях операции:

// actionTypes.js
export const ADD_TODO = 'ADD_TODO'
export const DELETE_TODO = 'DELETE_TODO'
export const EDIT_TODO = 'EDIT_TODO'
export const COMPLETE_TODO = 'COMPLETE_TODO'
export const COMPLETE_ALL = 'COMPLETE_ALL'
export const CLEAR_COMPLETED = 'CLEAR_COMPLETED'

Импортируем их в файл, в котором определяется создатель операции:

// actions.js
import { ADD_TODO } from './actionTypes'

export function addTodo(text) {
return { type: ADD_TODO, text }
}

И в редуктор:

import { ADD_TODO } from './actionTypes'

export default (state = [], action) => {
switch (action.type) {
case ADD_TODO:
return [
...state,
{
id: nanoid(),
text: action.text,
completed: false
}
]
default:
return state
}
}

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

В чем разница между redux-saga и redux-thunk?

Redux Thunk - это посредник (middleware), позволяющий вызывать создателей операции (action creators), возвращающих функции вместо объектов. Данные функции принимают метод dispatch, который используется для отправки обычной синхронной операции в теле функции после завершения асинхронных операций.

Redux Thunk

yarn add redux react-redux redux-logger redux-saga redux-thunk
# или
npm i redux react-redux redux-logger redux-saga redux-thunk

Thunk - это функция, принимающая некоторые параметры и возвращающая другую функцию, она принимает функции dispatch и getState и обе эти функции применяются в посреднике.

Базовая структура redux-thunk:

export const thunkName = (params) => (dispatch, getState) => {
// ...
}

Пример

import axios from "axios"
import GET_LIST_API_URL from "../config"

const fetchList = () => {
return (dispatch) => {
axios.get(GET_LIST_API_URL)
.then((response) => {
dispatch(getList(response.list))
})
.catch(console.error)
}
}

const getList = (payload) => {
return {
type: "GET_LIST",
payload
}
}

export { fetchList }

Redux Saga основан на генераторах, позволяющих писать асинхронный код, который выглядит как синхронный, и хорошо тестируется. В "саге" мы легко может тестировать наши асинхронные потоки данных и, при этом, наши операции остаются "чистыми". Она хорошо организует асинхронные операции, что повышает читаемость кода. Также "сага" имеет множество полезных инструментов для выполнения асинхронных операций.

Redux Saga

import axios from "axios"
import GET_LIST_API_URL from "../config"
import { call, put } from "redux-saga/effects"

const fetchList = () => {
return axios.get(GET_LIST_API_URL)
}

function_ fetchList () {
try {
const response = yield call(getCharacters)
yield put({ type: "GET_LIST", payload: response.list })
} catch (error) {
console.error(error.message)
}
}

export { fetchList }

Redux Thunk и Redux Saga предназначены для обработки побочных эффектов. Для выполнения данной задачи Thunk использует промисы, а Saga - генераторы. Thunk проще в использовании, и промисы лучше знакомы большинству разработчиков, Saga предоставляет больше возможностей, но нужно хорошо разбираться в генераторах. Рецепт такой: если хватает промисов, используем Thunk, если на постоянной основе приходится работать с более сложным кодом, переходим на Saga.

Приведите пример использования Redux Form

Ниже приводится простой пример подключения полей формы к redux-form.

Обычно, для этого достаточно обернуть каждое поле в компонент <Field>, определив тип компонента для рендеринга.

Компонент <Field> предоставляет пропы onChange, onBlur, onFocus, onDrag и onDrop для регистрации возникновения соответствующих событий, а также проп value для управления значением поля. Обратите внимание, что компонент SimpleForm не имеет состояния:

// SimpleForm.js
import React from 'react'
import { Field, reduxForm } from 'redux-form'

const SimpleForm = (props) => {
const { handleSubmit, pristine, reset, submitting } = props

return (
<form onSubmit={handleSubmit}>
<div>
<label>Имя</label>
<div>
<Field name="firstName" component="input" type="text" placeholder="Имя"/>
</div>
</div>
<div>
<label>Фамилия</label>
<div>
<Field name="lastName" component="input" type="text" placeholder="Фамилия"/>
</div>
</div>
<div>
<label>Email</label>
<div>
<Field name="email" component="input" type="email" placeholder="Email"/>
</div>
</div>
<div>
<label>Пол</label>
<div>
<label><Field name="sex" component="input" type="radio" value="male"/>Мужской</label>
<label><Field name="sex" component="input" type="radio" value="female"/>Женский</label>
</div>
</div>
<div>
<label>Любимый цвет</label>
<div>
<Field name="favoriteColor" component="select">
<option></option>
<option value="ff0000">Красный</option>
<option value="00ff00">Зеленый</option>
<option value="0000ff">Синий</option>
</Field>
</div>
</div>
<div>
<label htmlFor="employed">Трудоустроен</label>
<div>
<Field name="employed" id="employed" component="input" type="checkbox"/>
</div>
</div>
<div>
<label>Дополнительные сведения</label>
<div>
<Field name="notes" component="textarea"/>
</div>
</div>
<div>
<button type="submit" disabled={pristine || submitting}>Отправить</button>
<button type="button" disabled={pristine || submitting} onClick={reset}>Очистить</button>
</div>
</form>
)
}

export default reduxForm({
form: 'simple' // уникальный идентификатор формы
})(SimpleForm)

React Redux Form

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

Как выполнить сброс состояния?

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

import { combineReducers } from 'redux'
import AppReducer from './AppReducer'

import UsersReducer from './UsersReducer'
import OrderReducer from './OrderReducer'
import NotificationReducer from './NotificationReducer'
import CommentReducer from './CommentReducer'

/_
_ Для того, что сбросить состояние всех редукторов к начальному при выходе пользователя из системы,
_ `rootReducer` должен присвоить состоянию значение `undefined` при получении соответствующей операции
_
_ Если значением `state`, передаваемого редуктору, является `undefined`, то редуктор возвращает начальное состояние,
_ поскольку сигнатура редуктора выглядит следующим образом:
_ const reducer = (state = initialState, action) => { ... }
_/
const appReducer = combineReducers({
/_ редукторы уровня приложения _/
users: UsersReducer,
orders: OrderReducer,
notifications: NotificationReducer,
comment: CommentReducer,
})

const rootReducer = (state, action) => {
// отправка операции с указанным типом сбросит состояние приложения
if (action.type === 'USER_LOGOUT') {
state = undefined
}

return appReducer(state, action)
}

export default rootReducer

Почему функции в Redux называются редукторами?

Функции обновления состояния в Redux называются редукторами (reducers), потому что они похожи на функции, которые мы передаем в Array.prototype.reduce(). Они не просто возвращают "дефолтные" значения. Они всегда возвращают аккумулированное состояние (основанное на всех предыдущих и текущей операциях).

Они действуют подобно редукторам состояния. При каждом вызове редуктору передается операция с аргументами state и action. Затем на основании операции вычисляется и возвращается новое состояние. Это классический цикл функции fold() или reduce().

Как выполнить AJAX-запрос?

Для выполнения сетевых запросов используется один из следующих посредников:

  • Redux Promise
  • Redux Thunk
  • Redux Saga
  1. Redux Promise.

Это самый простой способ выполнения сетевого запроса в Redux. При использовании Redux Promise создатель операции возвращает промис внутри операции:

function getUserName(userId) {
return {
type: "SET_USERNAME",
payload: fetch(`/api/personal-details/${userId}`)
.then((response) => response.json())
.then((json) => json.userName)
.catch(console.error)
}
}

Этот посредник автоматически отправляет два события при выполнении сетевого запроса: SETUSERNAMEPENDING и SETUSERNAMEFULFILLED. Если возникает ошибка, отправляется SETUSERNAMEREJECTED.

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

  • Мы хотим выполнить асинхронную задачу минимальными усилиями.
  • Мы предпочитаем соглашение вместо настройки.
  • Наши сетевые запросы являются простыми.
  1. Redux Thunk.

Это стандартный способ выполнения сетевых запросов в Redux. При использовании Redux Thunk создатель операции возвращает функцию, принимающую метод dispatch в качестве аргумента:

function getUserName(userId) {
return (dispatch) =>
fetch(`/api/personalDetails/${userId}`)
.then((response) => response.json())
.then((json) => dispatch({ type: "SET_USERNAME", userName: json.userName }))
.catch(console.error)
}
}

Создатель операции вызывает dispatch() внутри then() после завершения асинхронной операции. Создатель операции может вызывать dispatch() столько раз, сколько потребуется.

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

  • Выполняется много сетевых запросов и требуется отправить много операций.
  • Нам нужен полный контроль над форматом операций.
  1. Redux Saga.

Это наиболее продвинутый способ выполнения сетевых запросов в Redux. В нем используются генераторы. При использовании Redux Saga сетевые запросы выполняются в "саге", а не в создателе операции. Вот как это выглядит:

import { call, put, takeEvery } from 'redux-saga/effects'

// вызываем `getUserName()` при отправке операции с типом `SET_USERNAME`
function_ mySaga() {
yield takeEvery("SET_USERNAME", getUserName)
}

function_ getUserName(action) {
try {
const user = yield call(fetch, `/api/personal-details/${userId}`)
yield put({ type: "SET_USERNAME_SUCCEEDED", user })
} catch ({ message }) {
yield put({ type: "SET_USERNAME_FAILED", message })
}
}

export default mySaga

"Саги" регистрируют отправляемые операции как обычные синхронные операции. В данном случае при отправке операции с типом SET_USERNAME выполняется "сага" getUserName(). Постфикс _ означает, что мы имеем дело с функцией-генератором. Для выполнения задач в таких функциях используется ключевое слово yield.

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

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

В чем разница между call() и put() в redux-saga?

Функции call и put предназначены для создания эффектов. call() используется для создания описания эффекта (effect description), указывающего посреднику вызвать промис. put() создает эффект, указывающий посреднику отправить операцию в хранилище.

Рассмотрим пример использования названных функций для получения данных пользователя:

function_ fetchUserSaga(action) {
// функция `call` принимает оставшиеся (rest) аргументы, переданные в функцию `api.fetchUser`, и
// указывает посреднику вызвать промис, разрешенное значение которого присваивается переменной `userData`
const userData = yield call(api.fetchUser, action.userId)

// указываем посреднику отправить соответствующую операцию
yield put({
type: 'FETCH_USER_SUCCESS',
userData
})
}

Определите ментальную модель redux-saga

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

yarn add redux-saga
# или
npm i redux-saga

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

class UserComponent extends React.Component {
// ...
onSomeButtonClicked() {
const { dispatch, userId } = this.props

dispatch({ type: 'USER_FETCH_REQUESTED', payload: { userId } })
}
// ...
}

Компонент отправляет объект операции в хранилище. Мы создаем "сагу" для наблюдения за операциями с типом USER_FETCH_REQUESTED и обращения к API для получения пользовательских данных:

import { call, put, takeEvery, takeLatest } from 'redux-saga/effects'
import api from './api'

// запускается при получении операции с типом `USER_FETCH_REQUESTED`
function_ fetchUser(action) {
try {
const user = yield call(api.fetchUser, action.payload.userId)
yield put({ type: "USER_FETCH_SUCCEEDED", user })
} catch (e) {
yield put({ type: "USER_FETCH_FAILED", message: e.message })
}
}

/_
Запуск `fetchUser()` при каждом получении операции с типом `USER_FETCH_REQUESTED`
позволяет одновременно запрашивать данные разных пользователей
_/
function_ mySaga() {
yield takeEvery("USER_FETCH_REQUESTED", fetchUser)
}

/_
В качестве альтернативы можно использовать `takeLatest()`, который
не позволяет одновременно получать данные нескольких пользователей
при получении операции с типом `USER_FETCH_REQUESTED` во время выполнения запроса.
Вместо этого, текущий запрос отменяется и отправляется новый
_/
function_ mySaga() {
yield takeLatest("USER_FETCH_REQUESTED", fetchUser)
}

export default mySaga

Для запуска "саги" необходимо подключить ее к хранилищу Redux с помощью посредника redux-saga.

import { createStore, applyMiddleware } from 'redux'
import createSaga from 'redux-saga'

import reducer from './reducers'
import mySaga from './sagas'

// создаем посредника
const sagaMiddleware = createSaga()
// внедряем его в хранилище
const store = createStore(
reducer,
applyMiddleware(sagaMiddleware)
)

// запускаем "сагу"
sagaMiddleware.run(mySaga)

// рендеринг приложения

Чем Relay отличается от Redux?

Redux - это стабильный контейнер для состояния JavaScript-приложений. Он помогает создавать приложения, которые ведут себя предсказуемо и запускаются в разном окружении. Состояние находится в единственном хранилище, каждый дочерний компонент имеет к нему доступ, изменение состояния осуществляется с помощью отправки операций. Redux не обрабатывает асинхронные операции из коробки, но это можно реализовать с помощью посредников.

Возможности Redux:

  • Предсказуемое состояние.
  • Легкое тестирование.
  • Работает как с React, так и с другими фреймворками и библиотеками.

Relay - разработан командой Facebook для React, используется во внутренних проектах компании. Похож на Redux в том, что оба имеют единственное хранилище. Основным отличием является то, что Relay управляет состоянием приложения через сервер, доступ к состоянию осуществляется через GraphQL-запросы (чтение и изменение данных). Relay кеширует и оптимизирует данные. С сервера запрашиваются только те данные, которые подверглись изменениям. Relay также поддерживает "оптимистические" обновления, т.е. изменения состояния до получения данных от сервера.

Возможности Relay:

  • Создание приложений, основанных на внешних данных.
  • Декларативный стиль.
  • Возможность модификации данных как на стороне клиента, так и на стороне сервера.

GraphQL - это сервисный фреймворк и протокол, использующий декларативные и комбинируемые запросы, решающий такие проблемы, как получение слишком большого количества данных или получение неполных данных, считается достойным кандидатом на замену REST API.

В каких случаях в React/Redux используется bindActionCreators()?

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

При использовании Redux с React библиотека react-redux предоставляет функцию dispatch, которая вызывается напрямую. Единственным случаем использования bindActionCreators() является передача создателей операции в дочерний компонент, который "не знает" о Redux.

Параметры:

  1. actionCreators (функция или объект): создатель операции или объект, значениями которого являются создатели операции.
  2. dispatch (функция): функция для отправки операций.

Возвращаемый результат

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

Пример

// TodoActionCreators.js
export function addTodo(text) {
return {
type: 'ADD_TODO',
text
}
}

export function removeTodo(id) {
return {
type: 'REMOVE_TODO',
id
}
}
// TodoListContainer.js
import { Component } from 'react'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'

import _ as TodoActionCreators from './TodoActionCreators'

console.log(TodoActionCreators)
// {
// addTodo: Function,
// removeTodo: Function
// }

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

const { dispatch } = props

// здесь у нас имеется подходящий случай для использования `bindActionCreators()`:
// мы не хотим, чтобы дочерний компонент "знал" о `Redux`
// мы создаем производные версии этих функций и позже
// передаем их потомку
this.boundActionCreators = bindActionCreators(TodoActionCreators, dispatch)
console.log(this.boundActionCreators)
// {
// addTodo: Function,
// removeTodo: Function
// }
}

componentDidMount() {
// зависимость, внедренная `react-redux`
const { dispatch } = this.props

// это не будет работать:
// TodoActionCreators.addTodo('Использовать Redux')

// это будет работать
const action = TodoActionCreators.addTodo('Использовать Redux')
dispatch(action)
}

render() {
// зависимость, внедренная `react-redux`
const { todos } = this.props

return <TodoList todos={todos} {...this.boundActionCreators} />

// в качестве альтернативы можно передать дочернему компоненту
// `dispatch()`, но в этом случае ему
// придется импортировать создателя операции

// return <TodoList todos={todos} dispatch={dispatch} />
}
}

export default connect(state => ({ todos: state.todos }))(TodoListContainer)

Что такое mapStateToProps() и mapDispatchToProps()?

Библиотека react-redux предоставляет 3 функции: connect, mapStateToProps и mapDispatchToProps. connect - это функция высшего порядка, принимающая mapStateToProps() и mapDispatchToProps() в качестве параметров.

Функция mapStateToProps извлекает состояние определенного редуктора из глобального хранилища и сопоставляет его с пропами компонента. Данная функция вызывается при каждом обновлении хранилища.

Функция mapDispatchToProp принимает функции отправки в компоненте и выполняет их в соответствующих редукторах. Данная функция позволяет отправлять изменения состояния в хранилище.

mapStateToProps: подключает состояние Redux к пропам React-компонента. mapDispatchToProps: подключает операции Redux к пропам React-компонента.

Пример

const { createStore } = Redux
const { connect, Provider } = ReactRedux
const initialState = { collection: ["prima", "altera", "triera"] }

function reducer(state = initialState, action) {
if (action.type === "REVERSE") {
return Object.assign({}, state, {
Collection: state.collection.slice().reverse()
})
}
return state
}

const store = createStore(reducer)

function mapStateToProps(state) {
return state
}

const PresentationalComponent = React.createClass({
render() {
return (
<div>
<h2>Состояние хранилища (в виде пропа)</h2>
<pre>{JSON.stringify(this.props.collection)}</pre>
<StateChangerUI />
</div>
)
}
})

// изменение состояния `UI`
const StateChangerUI = React.createClass({
// отправка операции
handleClick() {
store.dispatch({
type: 'REVERSE'
})
},
render() {
return (
<button type="button" className="btn btn-success" onClick={this.handleClick}>Инвертировать</button>
)
}
})

PresentationalComponent = connect(mapStateToProps)(PresentationalComponent)

ReactDOM.render(
<Provider store={store}>
<PresentationalComponent />
</Provider>,
document.getElementById('App')
)

Что такое Reselect?

Reselect - это простая библиотека для создания мемоизированных, комбинируемых функций-селекторов. Селекторы используются для эффективного вычисления производных данных из хранилища Redux.

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

Селекторы

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

const mapStateToProps = (state) => {
return {
activeData: getActiveData(state.someData, state.isActive)
}
}

Функция getActiveData возвращает из someData все записи со статусом isActive. Недостатком этой функции является то, что при любом изменении состояния она будет повторно вычислять данные.

При использовании Reselect входные аргументы кешируются в мемоизированной функции. Повторное вычисление производится только в случае изменения аргументов.

Пример

// todo.reducer.js
import { createSelector } from 'reselect'

const todoSelector = (state) => state.todo.todos
const searchTermSelector = (state) => state.todo.searchTerm

export const filteredTodos = createSelector(
[todoSelector, searchTermSelector],
(todos, searchTerm) =>
todos.filter(todo => todo.title.match(new RegExp(searchTerm, 'i')))
)

Мы можем использовать селекторы для получения всех задач (при отсутствии в состоянии searchTerm) или их отфильтрованного списка.

Какие существуют способы для отправки операций?

Redux - это стабильный контейнер для состояния JavaScript-приложений, в основном используемый с React. Он основан на операциях, которые отправляются и "прослушиваются" редукторами, изменяющими состояние.

  1. Передача метода dispatch в компонент.

Данный метод является методом объекта хранилища. Операция отправляется для запуска изменения состояния в хранилище:

// App.js
import { createStore } from 'redux'
import { MessageSender } from './MessageSender'
import reducer from './reducer'

const store = createStore(reducer)

class App extends React.Component {
render() {
<MessageSender store={store} />
}
}
// MessageSender.js
import { sendMsg } from './actions'

this.props.store.dispatch(sendMsg(msg))
  1. Использование React-Redux для создания глупых/умных компонентов.

Недостатком приведенного выше подхода является то, что наш React-компонент "не знает" о логике приложения. Лучшим решением является отделение логики в умном компоненте, подключенном к хранилищу, от UI, т.е. от глупого компонента.

Согласно официальной документации для connect() мы можем описать mapDispatchToProps() следующим образом: если передан объект, каждая функция, находящаяся в нем, считается создателем операции. Объект с такими же ключами, но с создателями операции, обернутыми в dispatch() для прямого вызова, объединяется с пропами компонента.

// MessageSenderContainer.js
import { connect } from 'react-redux'
import { sendMsg } from './actions'
import MessageSender from './MessageSender'

const mapDispatchToProps = { sendMsg }

export default connect(null, mapDispatchToProps)(MessageSender)

// MessageSender.js
this.props.sendMsg(msg)
  1. Использование метода bindActionCreators.

Метод bindActionCreators позволяет отправлять операции из React-компонентов, не подключенных к хранилищу Redux:

// MsgSenderPage.js
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import _ as actions from './actions'

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

const { dispatch } = props

this.boundedActions = bindActionCreators(actions, dispatch)
}

render() {
return <MsgSending {...this.boundedActions} />
}
}

export default connect()(MsgSenderPage)