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

Мы также добавим данные в наше приложение. Если вы знакомы с какой-либо разновидностью компонентного дизайна или паттернами MVC, то вы, вероятно, знаете, что смешивание в одном компоненте представлений и поведения приложения считается плохой практикой. Другими словами, представлениям нужны только данные для рендеринга, они не должны ничего знать о том, откуда эти данные, как они создаются и изменяются.

Получение данных с помощью Ajax

В качестве примера плохой практики расширим компонент UserList из предыдущей статьи так, чтобы он обрабатывал и извлекал данные:

// This is an example of tightly coupled view and data which we do not recommend

var UserList = React.createClass({
  getInitialState: function() {
    return {
      users: []
    }
  },

  componentDidMount: function() {
    var _this = this;
    $.get('/path/to/user-api').then(function(response) {
      _this.setState({users: response})
    });
  },

  render: function() {
    return (
      <ul className="user-list">
        {this.state.users.map(function(user) {
          return (
            <li key={user.id}>
              <Link to="{'/users/' + user.id}">{user.name}</Link>
            </li>
          );
        })}
      </ul>
    );
  }
});

Если вам необходимо более подробное описание того, что делает этот компонент, смотрите в разъяснении на GitHub.

Что не так с этим примером? Начнем с того, что в нем нарушается правило смешивания “поведения” и “вывода представления” — эти две вещи должны быть разделены.

Нет ничего плохого в использовании getInitialState для инициализации состояния компонента и нет ничего плохого в запросе Ajax из componentDidMount (хотя его стоит абстрагировать, чтобы он мог вызывать и другие функции). Проблема в том, что мы делаем все это вместе в одном компоненте, содержащем представление. Такая жесткая привязка делает приложение менее гибким и нарушает принцип DRY. А если вам понадобиться извлечь список пользователей откуда либо еще? Извлечение списка пользователей привязано к представлению, поэтому компонент нельзя использовать повторно.

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

Одной из таких альтернатив является Axios, основанный на промисах инструмент для работы с Ajax. Насколько они похожи?

// jQuery
$.get('/path/to/user-api').then(function(response) { ... });

// Axios
axios.get('/path/to/user-api').then(function(response) { ... });

Во всех оставшихся примерах мы будем продолжать использовать Axios. Но есть и другие аналогичные инструменты: got, fetch и SuperAgent.

Свойства и состояние

Перед тем, как мы перейдем к контейнерам и презентационным компонентам, нам надо разобраться со свойствами и состоянием.

Свойства и состояние связаны в том плане, что они являются “моделью” (данными) для компонентов React. И то, и другое может передаваться вниз от родительских к дочерним компонентам. Однако и свойства, и состояние родительского контейнера становятся только свойствами у дочерних компонентов.

Рассмотрим пример: ComponentA передает часть свойств и состояние к дочернему ComponentB. Метод render у ComponentA выглядит примерно так:

// ComponentA
render: function() {
  return <ComponentB foo={this.state.foo} bar={this.props.bar} />
}

Несмотря на то, что foo является состоянием у родительского компонента, оно становится свойством у дочернего компонента. Атрибут bar также становится свойством дочернего компонента, так как все данные, переданные от родительского компонента к потомкам становятся их свойствами. Следующий пример показывает, как метод ComponentB получает доступ к foo и bar в виде свойств:

// ComponentB
componentDidMount: function() {
  console.log(this.props.foo);
  console.log(this.props.bar);
}

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

Чтобы лучше понять концепцию состояния прочитайте документацию React. А в нашие статье изменяемые со временем данные будут упоминаться как “состояние”.

Разделение компонента

В примере с использованием Ajax мы создали проблему. Наш компонент UserList работает, но он хочет делать слишком много. Для решения этой проблемы мы разделим его на два компонента, каждый из которых будет играть различную роль. Эти два типа компонентов называются компоненты-контейнеры и презентационные компоненты, также их называют “умными” и “глупыми” компонентами соответственно.

Говоря кратко, компоненты-контейнеры содержат исходные данные и работают с состоянием. Состояние передается презентационным компонентам как свойство и затем рендерится в представление.

Термины “умные” и “глупые” компоненты постепенно уходят из употребления в сообществе. Я просто делаю упоминание о них, чтобы у вас не возникало лишних вопросов при чтении старых статей, теперь они известны как компоненты-контейнеры и презентационные компоненты.

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

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

var UserList = React.createClass({
  render: function() {
    return (
      <ul className="user-list">
        {this.props.users.map(function(user) {
          return (
            <li key={user.id}>
              <Link to="{'/users/' + user.id}">{user.name}</Link>
            </li>
          );
        })}
      </ul>
    );
  }
});

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

Презентационные компоненты “глупые” в том смысле, что они не имеют понятия о том, откуда взялись свойства, которыми они оперируют. Состояние? Нет, не слышали.

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

В React события прикрепляются непосредственно к представлению с помощью атрибутов типа onClick. И некоторых может удивить: как события работают с презентационным компонентом, не предназначенным для изменения свойств. Этому посвящен следующий раздел.

Итерации

При создании узлов DOM в цикле, атрибут key у элемента обязательно должен быть уникальным (относительно соседей). Это относится только к узлам DOM высшего уровня — в нашем случае <li>.

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

var UserList = React.createClass({
  render: function() {
    return (
      <ul className="user-list">
        {this.props.users.map(this.createListItem)}
      </ul>
    );
  },

  createListItem: function(user) {
    return (
      <li key={user.id}>
        <Link to="{'/users/' + user.id}">{user.name}</Link>
      </li>
    );
  }
});

Container Components

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

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

Для различения контейнера и презентационного компонента нам надо использовать разные имена, чтобы не было путаницы, контейнер будет называться UserListContainer:

var React = require('react');
var axios = require('axios');
var UserList = require('../views/list-user');

var UserListContainer = React.createClass({
  getInitialState: function() {
    return {
      users: []
    }
  },

  componentDidMount: function() {
    var _this = this;
    axios.get('/path/to/user-api').then(function(response) {
      _this.setState({users: response.data})
    });
  },

  render: function() {
    return (<UserList users={this.state.users} />);
  }
});

module.exports = UserListContainer;

Для краткости в наших примерах были пропущены декларации require() и module.exports. Но в данном случае важно показать, что презентационные компоненты в контейнерах указываются как прямые зависимости, поэтому в этом примере показан весь код для полноты картины.

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

Краткое примечание по стрелочным функциям ES6: вы могли заметить, что классический трюк с var _this = this нужен для этого примера. Стрелочные функции ES6 обладают не только более кратким синтаксисом, но и другими плюсами, позволяющими обходиться без подобного трюка. В этой серии статей, как правило, используется старый синтаксис ES5, чтобы вы могли сконцентрироваться на React, однако в руководстве на GitHub активно используется ES6, впрочем, с разъяснениями в файлах README.

События

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

Начнем разработку с добавления события в презентационный компонент (<button>, вы можете на нее нажать) напрямую, чтобы уяснить суть проблемы:

// Presentational Component
var UserList = React.createClass({
  render: function() {
    return (
      <ul className="user-list">
        {this.props.users.map(function(user) {

          return (
            <li key={user.id}>
              <Link to="{'/users/' + user.id}">{user.name}</Link>
              <button onClick={this.toggleActive}>Toggle Active</button>
            </li>
          );

        })}
      </ul>
    );
  },

  toggleActive: function() {
    // We shouldn't be changing state in presentational components :(
  }
});

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

В нашем примере изменяемым состоянием будет “активность” пользователя, но это может быть любая функция, которую вы хотите привязать к onClick.

Лучшим решением будет передача функциональности из контейнера в презентационный компонент в виде свойства, как на примере:

// Container Component
var UserListContainer = React.createClass({
  ...
  render: function() {
    return (<UserList users={this.state.users} toggleActive={this.toggleActive} />);
  },

  toggleActive: function() {
    // We should change state in container components :)
  }
});

// Presentational Component
var UserList = React.createClass({
  render: function() {
    return (
      <ul className="user-list">
      {this.props.users.map(function(user) {

        return (
          <li key={user.id}>
            <Link to="{'/users/' + user.id}">{user.name}</Link>
            <button onClick={this.props.toggleActive}>Toggle Active</button>
          </li>
        );

      })}
      </ul>
    );
  }
});

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

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

Вот демо, показывающее, как события контейнера изменяют состояние, что автоматически обновляет презентационный компонент:

See the Pen React Container Component Demo by Brad Westfall (@bradwestfall) on CodePen.

Обратите внимание, что этот пример работает с неизменяемыми данными и использует метод .bind().

Использование контейнеров с роутером

Роутер не должен больше использовать UserList напрямую. Вместо него роутер использует UserListContainer, который будет использовать UserList. В конечном итоге UserListContainer возвращает UserList и роутер будет получать то, что ему надо.

Поток данных и оператор расширения

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

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

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

В React версии 0.14 (выпущенной в конце 2015) появилась новая возможность создавать компоненты без состояния (презентационные компоненты) намного проще. Это называется функциональные компоненты без состояния (Stateless Functional Components).

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

// The older, more verbose way
var Component = React.createClass({

  render: function() {
    return (
      <div>{this.props.foo}</div>
    );
  }

});

// The newer "Stateless Functional Component" way
var Component = function(props) {
  return (
    <div>{props.foo}</div>
  );
};

Очевидно, что новый способ намного лаконичнее. Но помните, он подходит только компонентам с единственным методом render.

С новыми функциональными компонентами без состояния функция принимает аргумент со свойствами, это значит, что для доступа к свойствам не нужен this.

Вот очень хорошее видео с Egghead.io по функциональным компонентам без состояния.

MVC

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

Хотя это правда, в том, что в React нет “традиционных контроллеров”, в нем есть свои средства для разделения представлений и поведения. Я верю, что компоненты-контейнеры служат той же фундаментальной цели, что и контроллеры из традиционного MVC.

Что же насчет моделей, то я видел людей, использующих модели Backbone вместе с React и я уверен, что у них есть самые разные мнения о том, насколько хорошо это сочетается. Но я не уверен, что традиционные модели стоит использовать в React. Способ работы потока данных в React не совпадает с привычными моделями. Паттерн проектирования Flux, созданный Facebook, это способ соединения врожденной способности React передавать данные. В следующей части мы рассмотрим Redux, очень популярную имплементацию Flux, которую я вижу в качетсве альтернативы традиционным моделям.

Заключение

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

Эта статья написана под сильным влиянием других статей по этой теме. Обязательно ознакомьтесь с сопутствующими руководствами на Github для более полной информации и работающих примеров компонентов-контейнеров.