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

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

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

  1. Внутри компонента определяется начальное состояние state = { count: 0 }, с которым будет инициализирован компонент после отрисовки. Единственное требование к состоянию, которое предъявляет Реакт - тип данных: он должен быть объектом. То, что хранится внутри, определяется самим приложением.

    Альтернативный (и эквивалентный) способ задания начального состояния выглядит так:

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

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

  2. Функция render использует данные из state для отрисовки. Здесь никаких сюрпризов.

  3. На кнопку вешается обработчик на клик. В отличие от html, в свойство onClick передается функция, и она вызовется автоматически в момент срабатывания события. Внутри обработчика читается текущее значение счетчика, к нему прибавляется единица и далее идет установка нового состояния. Повторюсь: крайне важно не изменять state напрямую. Для установки нового состояния в реакте предусмотрена функция setState. Именно ее вызов приводит к тому, что компонент, в конце концов, перерисуется. Происходит это не сразу, то есть setState работает асинхронно и внутренняя магия пытается оптимизировать процесс рисования.

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

class Counter extends React.Component {
  onClick() {
    const count = this.state.count;
    this.setState({ count: count + 1 });
  };
}

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

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

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

По большому счету описанный выше механизм открывает практически все двери. Теперь вы с легкостью можете создавать интерактивные компоненты и оживлять ваш 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 + 1 });

// вторая кнопка
this.setState({ 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
]

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

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

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