Зарегистрируйтесь для доступа к 15+ бесплатным курсам по программированию с тренажером

Состояние JS: React

Вы уже основательно изучили JSX и неинтерактивный способ работы с компонентами React. С этого урока начинается самая главная часть: взаимодействие с пользователем. Ключевые понятия, которые будут рассмотрены в этом уроке: события и состояние. Пример:

See the Pen js_react_state_example by Hexlet (@hexlet) on CodePen.

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

  1. Внутри компонента, в конструкторе, определяется начальное состояние, с которым будет инициализирован компонент после отрисовки. Единственное требование к состоянию, которое предъявляет React — тип данных: он должен быть объектом. То, что хранится внутри, определяется самим приложением.

Способ задания начального состояния выглядит так:

class Clock extends React.Component {
  constructor(props) {
    super(props); // всегда обязательно
    this.state = { date: new Date() };
  }
}

Обратите внимание на то, что это единственное место, где state может изменяться напрямую (точнее, создаваться). Во всех остальных местах this.state должен использоваться только для чтения! Подробнее об этом дальше.

  1. Функция render использует данные из state для отрисовки. Здесь никаких сюрпризов.
  2. На кнопку вешается обработчик на клик. В отличие от HTML, в свойство onClick передается функция и она вызовется автоматически в момент срабатывания события. Внутри обработчика определяется текущая дата и идет установка нового состояния. Ещё раз: крайне важно не изменять state напрямую. Для установки нового состояния в React предусмотрена функция setState. Именно её вызов приводит к тому, что компонент, в конце концов, перерисуется. Происходит это не сразу, то есть setState работает асинхронно и внутренняя магия пытается оптимизировать процесс рисования.

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

class Clock extends React.Component {
  handleClick() {
    this.setState({ date: new Date() });
  }
}

Но такой подход плохо работает в React по двум причинам.

Первая заключается в том, что обработчики вызываются асинхронно, а методы в классах — это обычные функции с поздним связыванием. Поэтому нельзя просто так повесить обработчик, так как он потеряет this. С таким определением придется постоянно писать подобный код: onClick={this.handleClick.bind(this)} либо такой onClick={() => this.handleClick()}.

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

class Clock extends React.Component {
  handleClick = () => {
    this.setState({ date: new Date() });
  }
}

Ещё один пример:

See the Pen by Hexlet (@hexlet) on CodePen.

Логично ожидать, что счётчик будет увеличиваться на 2 при каждом клике, но этого не происходит. Как уже говорилось выше, this.setState выполняется Реактом не сразу. Следовательно, может возникнуть ситуация, когда state или props изменились к тому моменту, когда будет выполняться изменение состояния. Для таких случаев, когда новое состояние определяется на основе предыдущего (или на основе пропсов) у setState предусмотрена возможность принимать функцию вместо объекта:

this.setState((state, props) => {
  const { count } = state;

  return { count: count + 1 };
})

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

Попробуйте переписать пример со счётчиком, используя описанную форму setState.

По большому счёту, описанные выше механизмы открывают практически все двери. Теперь вы с лёгкостью можете создавать интерактивные компоненты и оживлять ваш UI. Всё остальное — это тонкости, предусмотренные для различных ситуаций.

Инициализация

Представьте, что в компоненте, созданном выше, нужно инициализировать счётчик со свойством count, переданным снаружи. И только в его отсутствие ставить 0. Для решения этой задачи нужно добавить две вещи:

  1. Использовать свойство count как начальное значение счетчика.
  2. Добавить значение по умолчанию для свойства count.

See the Pen js_react_state_init by Hexlet (@hexlet) on CodePen.

setState

В следующем примере реализованы две кнопки, каждая из которых управляет своим состоянием.

See the Pen js_react_state_set by Hexlet (@hexlet) on CodePen.

В данном примере объект состояния включает два свойства: count для одной кнопки и primary для другой. Основная хитрость этого примера заключается в процессе обновления состояния:

// первая кнопка
this.setState(({ count }) => ({ count: count + 1 }));

// вторая кнопка
this.setState(({ primary }) => ({ primary: !primary }));

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

Структура объекта состояния

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

const blogPosts = [
  {
    id : "post1",
    author : {username : "user1", name : "User 1"},
    body : "......",
    comments : [
      {
        id : "comment1",
        author : {username : "user2", name : "User 2"},
        comment : ".....",
      },
      {
        id : "comment2",
        author : {username : "user3", name : "User 3"},
        comment : ".....",
      }
    ]
  },
  {
    id : "post2",
    author : {username : "user2", name : "User 2"},
    body : "......",
    comments : [
      {
        id : "comment3",
        author : {username : "user3", name : "User 3"},
        comment : ".....",
      },
    ]
  }
  // and repeat many times
]

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

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

const state = {
  articles: [/*...*/],
  comments: [/*...*/],
}

Дополнительные материалы

  1. Нормализация данных

Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты

Об обучении на Хекслете

Для полного доступа к курсу нужен базовый план

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

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff
Рекомендуемые программы
профессия
от 6 300 ₽ в месяц
Разработка фронтенд-компонентов для веб-приложений
10 месяцев
с нуля
Старт 21 марта
профессия
от 9 900 ₽ в месяц
Разработка фронтенд- и бэкенд-компонентов для веб-приложений
16 месяцев
с нуля
Старт 21 марта

Используйте Хекслет по-максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»