Статьи

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

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

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

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

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

Вот репозиторий GitHub с демоверсией.

Итак, начнем.

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

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

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

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

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

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

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

Компонент Stateful vs Stateless против компонента с реквизитом

Хотя данные в подпорках доступны компоненту, философия React заключается в том, что подпорки должны быть неизменяемыми и нисходящими. Это означает, что родительский компонент может передавать любые данные, которые он хочет, своим дочерним элементам в качестве реквизитов, но дочерний компонент не может изменять свои реквизиты. Таким образом, если вы попытаетесь отредактировать реквизит, как я сделал ниже, вы получите ошибку типа «Невозможно назначить только для чтения».

1
2
3
4
5
6
const Button = (props) => {
    // props are read only
    props.count =21;
.
.
}

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

Компонент Stateful против компонента Stateless с состоянием

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

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

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

Stateful vs Stateless Components Функциональные компоненты

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

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

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

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

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
class Hello extends React.Component {
    constructor(props) {
        super(props);
    }
     
    render() {
        return(
            <div>
                Hello {props}
            </div>
        )
    }
}

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

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

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

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

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

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

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

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

1
2
3
4
5
// Here is an excerpt from the counter example
constructor(props) {
  super(props);
  this.state = { count: 0 };
}

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class App extends Component {
   
  /*
  // Not required anymore
  constructor() {
      super();
      this.state = {
        count: 1
      }
  }
  */
   
  state = { count: 1 };
   
  handleCount(value) {
      this.setState((prevState) => ({count: prevState.count+value}));
  }
 
  render() {
    // omitted for brevity
  }
   
}

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

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

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

Здесь ключевое слово this относится к экземпляру текущего компонента.

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

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

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

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

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

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

1
2
3
4
5
6
// What is the expected output?
handleCount(value) {
    this.setState({count: this.state.count+100});
    this.setState({count: this.state.count+value});
    this.setState({count: this.state.count-100});
}

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

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

1
(prevState, props) => stateChange

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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