Новый уровень React: Компоненты-контейнеры
React

Новый уровень React: Компоненты-контейнеры

Оригинал: Leveling Up With React: Container Components, Brad Westfall

Серия статей
Часть 1: React Router
Часть 2: Компоненты-контейнеры (вы здесь!)
Часть 3: Redux

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

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

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

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


/* Это пример сильно связанных данных и представления, делать так не рекомендуеся */

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>
    );
  }
});
Если вам нужно более подробное объяснение этого примера, посмотрите здесь.

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

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

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

Одна из таких альтернатив — Axios, инструмент для отправки ajax-запросов, основанный на промисах, очень похожий на jQuery. Правда похоже?


// 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, поскольку все данные, переданные в дочерний компонент, доступны в нем как свойства. Пример ниже показывает, как компонент ComponentB может получить доступ к свойствам foo и bar:


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

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

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

Пришло время разделения

В разделе Получение данных с Ajax мы создали проблему. Компонент UserList работает, но он пытается делать слишком много вещей. Чтобы решить эту проблему, разделим 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. Это требуется только для узлов высшего уровня, в данном случае — это элемент <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>
    );
  }
});

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

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

Компонент-контейнер и компонент-представление должны иметь разные имена, чтобы избежать путаницы, назовем контейнер 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, но не создает элементов DOM, а только возвращает компонент-представление.

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

События

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

Чтобы понять проблему, добавим событие в наш компонент-представление (клик по элементу <button>):


// Компонент-представление
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() {
    // Мы должны изменить состояние в компоненте-представлении :(
  }
});

Технически это будет работать, но это плохая идея.

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

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


// 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 задан в представлении, а функция вынесена в контейнер. Это более подходящее решение, поскольку контейнер отвечает за состояние.

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

Ниже показана демонстрация того, как по событию можно менять состояние компонента-контейнера, что автоматически вызывает обновление компонента-представления:

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

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

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

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

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

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

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

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

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

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


// Старый способ
var Component = React.createClass({

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

});

// Новый "Stateless Functional Component"
var Component = function(props) {
  return (
    <div>{props.foo}</div>
  );
};

Вы можете видеть, что новый способ является более компактным. Но помните, что этот способ подходит только для компонентов, состоящих из одного метода render.

Stateless-компонент принимает в качестве аргумента объект props. В этом случае не нужно использовать this для доступа к свойствам.

На Egghead.io есть отличное видео, посвященное stateless-компонентам.

MVC

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

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

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

Заключение

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

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

Рассылка
Подпишитесь на рассылку и получайте дайджест новостей и статей.
Никакого спама!
Подписаться