Возвращаемся к стейтам и погружаемся немного в ООП чтобы поработать с ними и лайфсайкл-методами
Окей, поняли что интерфейс от простого макета отличает интерактивность: человек может с ним взаимодействовать и фронтэнд должен обрабатывать эти события.
Но что делать, если нам в Реакте нужны события, которые не очень зависят от действий людей? Например, с компонентом что-то произошло и это нужно отследить? Как это сделать?
Для этого есть лайфсайкл-методы компонента: те методы, которые вызываются во время его жизни. Этих всадников апокалипсисов аж целых восемь и из их названий понятно, когда они вызываются:
Маунтинг (mounting) это момент, когда компонент, скажем так, собирается. Для этих состояний есть три стейта:
componentWillMount()
render()
(да, рендер по сути это тоже лайфсайкл-метод!)componentDidMount()
componentWillUnmount()
(список расположен по порядку вызова лайфсайклов)
componentWillReceiveProps(nextProps)
shouldComponentUpdate(nextProps, nextState)
— метод, в котором можно решить, нужно ли вызывать ре-рендер (обычно используется для оптимизаций)componentWillUpdate(nextProps, nextState)
render()
componentDidUpdate(prevState, prevProps)
Реакт бросит ошибку, если он не сможет построить интерфейс (например, вы ошиблись в типах и пытаетесь сделать .map()
по объекту), но как эту ошибку отловить? Лайфсайклом.
Что с ней потом делать? Можно в стейт поставить error: true
и пользователю показать плашку «Сорян, произошла ошибка», а саму ошибку поймать в какой-нибудь сервис типа Sentry или Bugsnag.
// src/Movies/index.js
import React from 'react';
import bugsnag from 'bugsnag';
import Movies from './List';
import { Alert } from '../ui';
class extends React.Component {
componentDidCatch(error, info) {
bugsnag.notify(error);
}
render() {
return (
<section>
<Alert>
Произошла ошибка, мы уже отправили сообщение разработчикам
</Alert>
<Movies data={data} />
</section>
)
}
}
Кстати, у Багснега есть даже специальный компонент для Error Boundaries — это концепт работы ошибок, который появился в Реакте 16.
Но что это за синтаксис странный? Почему мы написали функцию componentDidCatch() {}
без function
, да ещё и обернули в class
?
В позапрошлом уроке мы говорили про стейты, но там мы работали с псевдокодом — да, я действительно не показал реальный код, потому что для этого было рано. Сейчас — самое время.
Окей, вот мы работаем с компонентом и всё понятно, когда речь идёт про вёрстку: возвращай через return
Джсх, Реакт построит интерфейс из всех компонентов.
Но что делать со стейтами? Или лайфсайклами? Если функции это просто вызываемый кусок кода, то куда это всё впихнуть? Где хранить стейт? Как подписаться на лайфсайклы?
Для этого есть классы — специальные объекты, в которых могут быть методы и поля. Пример?
class Rectangle {
constructor(height, width) {
this.height = height;
this.width = width;
}
calcTotal() {
return this.height * this.width;
}
}
const square = new Rectangle(10, 10);
console.log(square); // Rectangle { width: 10, height: 10 }
console.log(square.calcTotal()); // 100
Надо разбираться!
Классы пришли из объекто-ориентированного программирования — подхода, со временем показавшего свою несостоятельность перед функциональным программированием.
Но не верьте мне на слово, а то потом на собеседованиях будете отвечать «ООП говно потому что Родионов так говорит», что, по сути, карго-культ слепой. Попробуйте оба подхода и вернитесь к ФП. Красноречивее всего, конечно, этот слайд из выступления Functional Programming Design Patterns.
Этот слайд говорит о том, что пока люди в ОО придумывают новые паттерны программирования, люди в ФП просто пишут код на функциях и не устраивают баталии «этот код нужно писать на фактори паттерне, а не на визитор!» на код-ревью.
Окей, но у нас есть классы в Реакте как способ инкапсуляции компонента и нам нужно с этим что-то делать. Придётся изучать (хотя совсем недавно вышел @reactions/component и слава Райану, можно уйти от классов).
constructor
в нашем примере это по сути такой же лайфсайкл-метод, но не Реакта, а просто класса. В него аргументом приходит то, что вы передали при инициализации класса через new MySuperClass(...)
. При инициализации создаётся экземпляр класса. Тяжко? Понимаю.
Следующее незнакомое нам это this
. this
это специальный объект, в котором есть данные, которые принадлежат этому скоупу. Пример с функциями:
(function parent() {
this.x = 1;
console.log(this.x); // 1
(function child() {
console.log(this.x); // 1
this.x = 2;
console.log(this.x); // 2
})();
console.log(this.x);
})();
Функции обёрнуты в
()()
чтобы их сразу же вызвать — вторыми скобками мы вызываем как будтоmyFunc()
, но сначала нужно обернуть в скобки, чтобы не былоfunction {}()
— интерпретатор не поймёт что мы от него хотим и откуда взялись()
.
Но почему у вложенной функции на первом консольлоге всё равно доступен this
родительской?
Это кложа (closure, на русском обычно говорят "замыкание") — функция со всеми внешними данными, что ей доступны. Второй пример на кложу:
const x = 1;
(function() {
const y = 2;
console.log(x); // 1
(function() {
const z = 3;
console.log(y); // 2
console.log(z); // 3
})();
console.log(z); // ReferenceError: z is not defined
})();
console.log(y); // ReferenceError: y is not defined
console.log(z); // ReferenceError: z is not defined
Почему y
и z
недоступны? Потому что это внутренние константы тех двух функций.
Окей, разобрались с кложами и this
, разве что добавлю, что у каждого экземпляра класса свой this
:
const square = new Rectangle(10, 10);
const rectangle = new Rectangle(100, 203);
console.log(square); // Rectangle { width: 10, height: 10 }
console.log(rectangle); // Rectangle { width: 100, height: 203 }
Второе, что нас интересует в классах — наследование. Наследование делается через синтаксис class extends X {}
и наследование означает, что методы и свойства класса X перейдут в новый. Для этого нужно в своём классе вызвать super()
в конструкторе.
class X {
calcTotal() {
return this.height * this.width;
}
}
class Rectangle extends X {
constructor(height, width) {
super(height, width);
this.height = height;
this.width = width;
}
}
const square = new Rectangle(10, 10);
const rectangle = new Rectangle(100, 203);
console.log(square.calcTotal()); // 100
console.log(rectangle.calcTotal()); // 20300
Окей, вроде всё более-менее понятно, хоть и бесполезно, давайте возвращаться в лоно Реакта и писать нормально.
Всё это погружение в классы нужно было, чтобы познакомить вас с классовыми компонентами.
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
class WelcomeClassed extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
Как видите, разница в случае рендера не особо велика: ну подумаешь, обернули в класс и прописали метод render()
, а пропы из аргументов переехали в this
.
Кстати, вы же знаете, что каждый раз this
будет разным для каждого вызова одного и того же компонента? Теперь знаете.
class Img extends React.Component {
render() {
console.log(this.props);
return <img src={this.props.src} />;
}
}
<Img src="https://i.imgur.com/wycmlAg.jpg" /> // { src: "https://i.imgur.com/wycmlAg.jpg" }
<Img src="https://i.imgur.com/82kaBeI.jpg" /> // { src: "https://i.imgur.com/82kaBeI.jpg" }
Окей, мы отнаследовались от React.Component
, что нам это даёт? Правильно, работу со стейтами и лайфсайклы-методы!
Начальный стейт компонента объявляется как поле или в конструкторе
class extends React.Component {
// стейт объявляется как поле
state = {}
// либо в конструкторе, но зачем
constructor(props) {
super(props);
this.state = {};
}
}
Стейт обновляется через метод this.setState()
, в который нужно передать новый стейт. Что? Где он, если мы его не объявили? Явное лучше неявного? Добро пожаловать в ООП, где так не думают!
this.setState()
нам подарен экстендом React.Component
. Где его можно вызывать? Везде, кроме рендера
Лайфсайклы это методы вашего класса и они вызываются самим Реактом.
// src/Movies/List.js
import React from 'react';
import Card from './Card';
import { Loading } from '../ui';
class Movies extends React.Component {
// начальный стейт
state = {
list: [],
isLoading: false
}
// почему didMount вместо willMount?
// https://daveceddia.com/where-fetch-data-componentwillmount-vs-componentdidmount/
componentDidMount() {
// поставим что мы отправили запрос
this.setState({ isLoading: true });
// отправляем запрос к АПИ
const response = fetch("/api/v1/movies");
// ставим в стейт ответ
this.setState({ list: response.list });
}
render() {
// render() вызывается на каждом обновлении стейта или пропов
// поэтому Реакт сам решит что ему показать
// мы же описываем интерфейс от стейта, состояния компонента
return (
<section>
{this.state.isLoading && <Loading>...</Loading>}
{this.state.list.map(movie => {
return <Card data={movie} />
});
</section>
)
}
}
Нужно ли вызывать класс через new Movies()
? Нет, используйте как обычный компонент через Джсх или React.createElement()
, а работу с функциональными или классовыми компонентами Реакт возьмёт на себя.
Урок получился сложным — это камень в огород ООП. Когда мы вернулись к Реакту, стало всё намного лучше и понятнее.
Сегодня мы разобрались с:
- лайфсайклами,
- Error Boundaries,
- классами,
this
,- кложами (они же замыкания),
- наследованиями,
- работой со всем этим в Реакте
Главный вывод: в большинстве случаев нужно использовать функциональные компоненты, а когда нужны стейты или лайфсайкл-методы — классы. Либо смотреть в сторону reactions/component.
Кстати, мы в уроке не разобрались с вопросом а когда всё же нужны стейты или лайфсайклы.
Когда у компонента должно быть своё состояние. Старайтесь не размазывать по двадцати компонентам то, что можно хранить в одном и спускать пропами, но и свалку не устраивайте.
Обычно нужны три лайфсайкла: componentDidMount()
, componentWillReceiveProps(nextProps)
и componentDidCatch(error, info)
.
componentDidMount()
— при загрузке компонента (например, сменили страницу в роутере),componentWillReceiveProps(nextProps)
— когда изменились данные (например, сменили страницу в роутере, компонент остался тем же, а данные новые),componentDidCatch(error, info)
— когда поймали ошибку.