Advertisement
  1. Code
  2. React

Stateful и Функциональные stateless компоненты в React

Scroll to top
Read Time: 11 min

() translation by (you can also view the original English article)

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

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

Необходимые условия

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

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

Давайте начнем.

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

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

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

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

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

1
 <div>
2
    Current count: {this.state.count}
3
    <hr />
4
    {/* Component reusability in action. */ }
5
    <Button sign = "+" count={this.state.count} 
6
        updateCount = {this.handleCount.bind(this) }/>

7
    <Button sign = "-" count={this.state.count}  
8
        updateCount = {this.handleCount.bind(this) }/>

9
</div>

Props и State

Компонентам нужны данные для работы. Существует два разных способа объединения компонентов и данных: либо в виде props, либо в виде state. props и state определяют, что делает компонент и как он себя ведет. Начнем с props.

props

Если компонентами это простые JavaScript функции, то props это входные параметры функции. Исходя из этой аналогии, компонент принимает вход (то, что мы называем props), обрабатывает его, а затем реднерит некоторый JSX код .

Stateful vs Stateless Component Component with propsStateful vs Stateless Component Component with propsStateful vs Stateless Component Component with props

Хотя данные в props доступны для компонента, философия React заключается в том, что props должен быть неизменным и сверху вниз. Это означает, что родительский компонент может передавать любые данные, которые он хочет своим детям, в качестве props, но дочерний компонент не может изменять свои props. Итак, если вы попытаетесь изменить props, как я сделал ниже, вы получите "Cannot assign to read-only" TypeError.

1
const Button = (props) => {
2
    // props are read only

3
    props.count =21;
4
.
5
.
6
}

State

С другой стороны, State является объектом, принадлежащим компоненту, где он объявлен. Его область ограничена текущим компонентом. Компонент может инициализировать свое состояние и обновлять его, когда это необходимо. Состояние родительского компонента обычно становится props дочернего компонента. Когда состояние передается из текущей области, мы называем его prop.

Stateful vs Stateless Component Component with StateStateful vs Stateless Component Component with StateStateful vs Stateless Component Component with State

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

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

Компонент React может быть двух типов: либо это компонент в виде класса, либо функциональный компонент. Разница между ними очевидна из их имен.

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

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

Stateful vs Stateless Components Functional ComponentsStateful vs Stateless Components Functional ComponentsStateful vs Stateless Components Functional Components

Некоторые разработчики предпочитают использовать новые стрелочные функции ES6 для определения компонентов. Стрелочные функции более компактны и предлагают сжатый синтаксис для написания функций выражений. Используя стрелочные функции, мы можем пропустить использование двух ключевых слов, function, return и пары фигурных скобок. С помощью нового синтаксиса вы можете определить компонент в одной строке следующим образом.

1
const Hello = ({ name }) => (<div>Hello, {name}!</div>);

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

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

Синтаксис state = {count: 1} является частью публичных полей класса. Подробнее об этом ниже.

Существует два способа создания класса компонента. Традиционным способом является использование React.createClass(). ES6 ввел синтаксический сахар, который позволяет писать классы, расширяющие React.Component. Однако оба метода предназначены для того, чтобы делать то же самое.

Компоненты класса могут существовать и без state. Ниже приведен пример компонента класса, который принимает на вход props и отображает JSX.

1
class Hello extends React.Component {
2
    constructor(props) {
3
        super(props);
4
    }
5
    
6
    render() {
7
        return(
8
            <div>
9
                Hello {props}
10
            </div>

11
        )
12
    }
13
}

Мы определяем метод конструктора, который принимает props в качестве входных данных. Внутри конструктора мы вызываем super(), чтобы передать все, что унаследовано от родительского класса. Вот несколько деталей, которые вы, возможно, пропустили.

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

Компоненты в виде класса всегда должны вызывать базовый конструктор с props.

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

Во-вторых, если вы используете конструктор, вам нужно вызвать super(). Это обязательно, в противном случае вы получите синтаксическую ошибку "Missing super() call in constructor".

И последнее замечание касается использования super() и super(props). super(props) следует использовать, если вы собираетесь вызвать this.props внутри конструктора. В противном случае достаточно использовать только super().

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

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

Компоненты имеющие состояние

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

1
// Here is an excerpt from the counter example

2
constructor(props) {
3
  super(props);
4
  this.state = { count: 0 };
5
}

Мы создали объект state и инициализировали его с числом 0. Существует альтернативный синтаксис, предлагаемый для упрощения называемый class fields. Это еще не входит в спецификацию ECMAScript, но если вы используете транспилер Babel, этот синтаксис должен работать из коробки.

1
class App extends Component {
2
  
3
  /*

4
  // Not required anymore

5
  constructor() {

6
      super();

7
      this.state = {

8
        count: 1

9
      }

10
  }

11
  */
12
  
13
  state = { count: 1 };
14
  
15
  handleCount(value) {
16
      this.setState((prevState) => ({count: prevState.count+value}));
17
  }
18
19
  render() {
20
    // omitted for brevity

21
  }
22
  
23
}

В целом вы можете избежать использования конструктора с помощью этого нового синтаксиса.

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

1
render() {
2
return (
3
    Current count: {this.state.count}
4
    )
5
}

Ключевое слово this ссылается к экземпляру текущего компонента.

Инициализации state недостаточно - нам нужно обновить state, чтобы создать интерактивное приложение. Если вы считаете, что это сработает, нет, это не так.

1
//Wrong way

2
3
handleCount(value) {
4
    this.state.count = this.state.count +value;
5
}

Компоненты React оснащены методом setState для обновления state. setState принимает объект, который содержит новый state count.

1
// This works

2
3
handleCount(value) {
4
    this.setState({count: this.state.count+ value});
5
}

SetState() принимает объект как входной параметр, и мы увеличиваем предыдущее значение count на 1, которое работает как ожидалось. Однако есть улов. Когда есть несколько вызовов setState, которые читают предыдущее значение state и записывают в него новое значение, мы можем столкнуться с состоянием гонки. Это означает, что конечные результаты не будут соответствовать ожидаемым значениям.

Вот пример, который должен дать вам понимание. Попробуйте это в приведенном выше сниппете codeandbox.

1
// What is the expected output? Try it in the code sandbox.

2
handleCount(value) {
3
    this.setState({count: this.state.count+100});
4
    this.setState({count: this.state.count+value});
5
    this.setState({count: this.state.count-100});
6
}

Мы хотим, чтобы setState увеличивал счет на 100, а затем обновлял его на 1, а затем удалял 100, которые были добавлены ранее. Если setState выполняет переход состояния в приведенном порядке, мы получим ожидаемое поведение. Тем не менее, setState является асинхронным, и несколько вызовов setState могут быть собраны вместе для лучшего поведения и производительности UI. Таким образом, приведенный выше код дает поведение, отличное от ожидаемого.

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

1
(prevState, props) => stateChange
2

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

1
// The right way

2
3
handleCount(value) {
4
    
5
  this.setState((prevState) => {
6
    count: prevState.count +1
7
  });
8
}

SetState() перерендеривает компонент, и у вас есть работающий stateful компонент.

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

Вы можете использовать либо функцию, либо класс для создания компонентов без состояния. Но если вам не нужно использовать хуки жизненного цикла в ваших компонентах, желательно перейти на функциональные компоненты без состояния. Есть много преимуществ, если вы решили использовать функциональные компоненты без state; они просты в написании, понимании и тестировании, и вы можете вообще избегать ключевого слова this. Однако, как и в случае с React v16, преимущества использования функциональных компонентов без учета состояния над компонентами класса отсутствуют.

Недостатком является то, что вы не можете использовать хуки жизненного цикла. Метод жизненного цикла ShouldComponentUpdate() часто используется для оптимизации производительности и для ручного управления тем, что получает рендеринг. Вы не можете использовать это с функциональными компонентами. Refs также не поддерживаются.

Контейнерные компоненты и Презентационные компоненты

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

Компоненты презенторы

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

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

Функциональные компоненты должны быть вашим первым выбором для написания презентационных компонентов, если не требуется состояние. Если презентационный компонент требует состояния, он должен быть связан с состоянием пользовательского интерфейса, а не с фактическими данными. Презентационный компонент не взаимодействует с хранилищем Redux и не выполняет вызовы API.

Компоненты контейнера

Компоненты контейнера будут иметь дело с поведенческой частью. Компонент контейнера сообщает презентационному компоненту, что нужно рендерить с помощью props. Он не должен содержать ограниченные DOM разметки и стили. Если вы используете Redux, компонент контейнера содержит код, который отправляет действие в хранилище. В качестве альтернативы, это место, где вы должны поместить свои вызовы API и сохранить результат в state компонента.

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

Итак, что такое PureComponent?

Вы очень часто слышите термин «чистый компонент» в кругах React, а это React.PureComponent. Когда вы новичок в React, все это может показаться немного запутанным. Компонент считается чистым, если гарантированно возвращает тот же результат, учитывая тот же props и state. Функциональный компонент является хорошим примером чистого компонента, потому что, учитывая входные параметры, вы знаете, что будет рендерится.

1
const HelloWorld = ({name}) => (
2
 <div>{`Hi ${name}`}</div>

3
);

Компоненты класса могут быть чистыми, если их props и state иммутабельны. Если у вас есть компонент с «глубоким» неизменным набором props и state, React API имеет нечто, называемое PureComponent. React.PureComponent похож на React.Component, но он реализует метод ShouldComponentUpdate() несколько иначе. ShouldComponentUpdate() вызывается до того, как что-то перерендерилось. Поведение по умолчанию заключается в том, что оно возвращает true, так что любое изменение state или props перерендерит компонент.

1
shouldComponentUpdate(nextProps, nextState) {
2
  return true;
3
}

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

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

Заключение

Функциональные компоненты без state более элегантны и обычно являются хорошим выбором для создания презентационных компонентов. Поскольку они - просто функции, вам не составит труда писать и понимать их, и, кроме того, они легко тестируются.

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

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

За последние пару лет React стал популярным. Фактически, у нас есть ряд материалов на рынке Envato, которые доступны для покупки, просмотра, реализации и т. д. Если вы ищете дополнительные ресурсы вокруг React, не стесняйтесь ознакомится с ними.

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.