Мыслим в стиле React

image


Перевод туториала официальной документации библиотеки React.js.


Мыслим в стиле React


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


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



Начнем с макета


Представим, что у нас имеется некое JSON API и макет от нашего дизайнера. Макет выглядит следующим образом:


Макет


Наше JSON API возвращает некоторые данные в следующем виде:


[
  {category: "Sporting Goods", price: "$49.99", stocked: true, name: "Football"},
  {category: "Sporting Goods", price: "$9.99", stocked: true, name: "Baseball"},
  {category: "Sporting Goods", price: "$29.99", stocked: false, name: "Basketball"},
  {category: "Electronics", price: "$99.99", stocked: true, name: "iPod Touch"},
  {category: "Electronics", price: "$399.99", stocked: false, name: "iPhone 5"},
  {category: "Electronics", price: "$199.99", stocked: true, name: "Nexus 7"}
];

Шаг 1: Переводим пользовательский интерфейс в иерархию Компонентов


Первое, что мы делаем, это рисуем на макете боксы вокруг каждого компонента (и подчиненных компонентов) и присваиваем им названия. Если вы работаете с дизайнером, он, возможно, уже сделал это, так что необходимо поговорить с ним! Может оказаться, что названия слоев в Photoshop вполне подойдут для наименования ваших React компонентов!


Но откуда узнать, каким должен быть конечный отдельный компонент? Просто используйте те же методы для определения, что и при создании новой функции или объекта. Одним из таких методов является Принцип единственной ответственности, то есть компонент, в идеале, должен создавать только одну вещь/сущность. Если компонент создает несколько повторяемых в других компонентах сущностей, то он должен быть разложен на более мелкие компоненты.


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


Диаграмма компонентов


Как видно из макета — у нас есть пять компонентов в нашем простом приложении. Мы выделили разноцветными боксами каждый из компонентов.


  1. FilterableProductTable (оранжевый): содержит всю нашу таблицу
  2. SearchBar (синий): принимает весь пользовательский ввод
  3. ProductTable (зеленый): отображает и фильтрует набор данных основанных на пользовательском вводе
  4. ProductCategoryRow (бирюзовый): отображает заголовок для каждой категории
  5. ProductRow (красный): отображает строку для каждого товара

Если вы посмотрите на ProductTable, вы увидите, что заголовок таблицы (содержащий ярлыки "Name" и "Price") не выделен в отдельный компонент. Это вопрос предпочтений, и есть аргументы для того или иного варианта. Для этого примера, мы оставили заголовок в составе ProductTable, потому что он является частью визуализации набора данных, которая относится к ответственности ProductTable. Однако, если заголовок был бы более сложным (например: мы должны были бы добавить возможность для сортировки по столбцам), конечно, мы бы выделили бы его в отдельный компонент ProductTableHeader.


Теперь, когда мы определили компоненты в нашем макете, давайте оформим их в виде иерархии. Это легко. Компоненты, которые входят в другой компонент в макете, должны выглядеть как потомки в иерархии:


  • FilterableProductTable
    • SearchBar
    • ProductTable
      • ProductCategoryRow
      • ProductRow

Шаг 2: Создаем статическую версию в React



HTML
<div id="container">
    <!-- Содержание этого элемента будет заменено вашим компонентом. -->
</div>

CSS
body {
    padding: 5px
}

JavaScript
class ProductCategoryRow extends React.Component {
  render() {
    return <tr><th colSpan="2">{this.props.category}</th></tr>;
  }
}

class ProductRow extends React.Component {
  render() {
    var name = this.props.product.stocked ?
      this.props.product.name :
      <span style={{color: 'red'}}>
        {this.props.product.name}
      </span>;
    return (
      <tr>
        <td>{name}</td>
        <td>{this.props.product.price}</td>
      </tr>
    );
  }
}

class ProductTable extends React.Component {
  render() {
    var rows = [];
    var lastCategory = null;
    this.props.products.forEach(function(product) {
      if (product.category !== lastCategory) {
        rows.push(<ProductCategoryRow category={product.category} key={product.category} />);
      }
      rows.push(<ProductRow product={product} key={product.name} />);
      lastCategory = product.category;
    });
    return (
      <table>
        <thead>
          <tr>
            <th>Name</th>
            <th>Price</th>
          </tr>
        </thead>
        <tbody>{rows}</tbody>
      </table>
    );
  }
}

class SearchBar extends React.Component {
  render() {
    return (
      <form>
        <input type="text" placeholder="Search..." />
        <p>
          <input type="checkbox" />
          {' '}
          Only show products in stock
        </p>
      </form>
    );
  }
}

class FilterableProductTable extends React.Component {
  render() {
    return (
      <div>
        <SearchBar />
        <ProductTable products={this.props.products} />
      </div>
    );
  }
}

var PRODUCTS = [
  {category: 'Sporting Goods', price: '$49.99', stocked: true, name: 'Football'},
  {category: 'Sporting Goods', price: '$9.99', stocked: true, name: 'Baseball'},
  {category: 'Sporting Goods', price: '$29.99', stocked: false, name: 'Basketball'},
  {category: 'Electronics', price: '$99.99', stocked: true, name: 'iPod Touch'},
  {category: 'Electronics', price: '$399.99', stocked: false, name: 'iPhone 5'},
  {category: 'Electronics', price: '$199.99', stocked: true, name: 'Nexus 7'}
];

ReactDOM.render(
  <FilterableProductTable products={PRODUCTS} />,
  document.getElementById('container')
);

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


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


Вы можете реализовывать приложение сверху-вниз или снизу-вверх. То есть, вы можете начать с построения Компонентов более высокого уровня иерархии (начиная с FilterableProductTable) или наоборот с низкого уровня (ProductRow). В более простых приложениях, как правило, легче идти сверху вниз, а в более крупных — снизу вверх и параллельно писать тесты по мере реализации Компонентов.


В конце этого шага, у вас будет библиотека повторно используемых Компонентов, которые отображают вашу модель данных. Компоненты будут иметь только метод render(), так как это статическая версия. Компонент в верхней части иерархии (FilterableProductTable) получит модель данных посредством props. Если вы внесете изменение в базовую модель данных и вызовете ReactDOM.render() заново — пользовательский интерфейс будет обновлен. Не правда ли — в этом нет ничего сложного, т.к это действительно очень просто? Односторонний поток данных React (также называемый односторонним связыванием) обеспечивает модульность и скорость.


Небольшое отступление: Свойства (props) и Состояние (state)


В React существуют две "модели" данных: props и state. Важно понимать различие между ними. Если вы не уверены, что знаете разницу — перечитайте соответствующий раздел официальной документации.


Шаг 3: Определяем минимальное (но достаточное) представление Cостояния (state) пользовательского интерфейса


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


Для построения корректного приложения, в первую очередь, вам необходимо обдумать минимальный набор изменяемых состояний необходимых для вашего приложения. Придерживайтесь принципа DRY: Don't Repeat Yourself (Не повторяйся). В идеале — минимальное представление Состояния вашего приложения не должно содержать чего либо, что может быть вычислено на основании имеющихся данных (в props и state) в момент времени, когда это необходимо. Например: Если вы строите приложение, отображающее список дел, оставайтесь в пределах массива, содержащего записи дел — не создавайте отдельной переменной Состояния, отображающей количество дел. Вместо этого, в тот момент когда вам необходимо отобразить количество дел — просто возьмите длину имеющегося массива.


Продумаем все единицы данных в нашем приложении. Мы имеем:


  • Оригинальный список продуктов
  • Поисковый текст, введенный пользователем
  • Значение чекбокса
  • Отфильтрованный список продуктов

Давайте пройдемся по каждому пункту и определим — является ли он Состоянием. Для каждой единицы данных нам надо задать три вопроса:


  1. Передаются ли эти данные посредством props от предка? Если да, то это скорее всего не Состояние.
  2. Остаются ли эти данные неизменными с течением времени? Если да, то это скорее всего не Состояние.
  3. Можете ли вы вычислить эти данные на основании имеющихся (в props и state) в вашем Компоненте? Если да, то это не Состояние.

Оригинальный список продуктов передается посредством props — значит это не Состояние. Поисковый текст и значение чекбокса могут изменяться с течением времени и не могут быть вычислены из имеющихся данных — это Состояния. И последнее: Отфильтрованный список продуктов может быть вычислен комбинированием оригинального списка продуктов, поисковым текстом и значением чекбокса — это не Состояние.


В итоге, наши Состояния:


  • Поисковый текст, введенный пользователем
  • Значение чекбокса

Шаг 4: Определяем где наше Состояние будет размещаться



HTML
<div id="container">
    <!-- Содержание этого элемента будет заменено вашим компонентом. -->
</div>

CSS
body {
    padding: 5px
}

JavaScript
class ProductCategoryRow extends React.Component {
  render() {
    return (<tr><th colSpan="2">{this.props.category}</th></tr>);
  }
}

class ProductRow extends React.Component {
  render() {
    var name = this.props.product.stocked ?
      this.props.product.name :
      <span style={{color: 'red'}}>
        {this.props.product.name}
      </span>;
    return (
      <tr>
        <td>{name}</td>
        <td>{this.props.product.price}</td>
      </tr>
    );
  }
}

class ProductTable extends React.Component {
  render() {
    var rows = [];
    var lastCategory = null;
    this.props.products.forEach((product) => {
      if (product.name.indexOf(this.props.filterText) === -1 || (!product.stocked && this.props.inStockOnly)) {
        return;
      }
      if (product.category !== lastCategory) {
        rows.push(<ProductCategoryRow category={product.category} key={product.category} />);
      }
      rows.push(<ProductRow product={product} key={product.name} />);
      lastCategory = product.category;
    });
    return (
      <table>
        <thead>
          <tr>
            <th>Name</th>
            <th>Price</th>
          </tr>
        </thead>
        <tbody>{rows}</tbody>
      </table>
    );
  }
}

class SearchBar extends React.Component {
  render() {
    return (
      <form>
        <input type="text" placeholder="Search..." value={this.props.filterText} />
        <p>
          <input type="checkbox" checked={this.props.inStockOnly} />
          {' '}
          Only show products in stock
        </p>
      </form>
    );
  }
}

class FilterableProductTable extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      filterText: '',
      inStockOnly: false
    };
  }

  render() {
    return (
      <div>
        <SearchBar
          filterText={this.state.filterText}
          inStockOnly={this.state.inStockOnly}
        />
        <ProductTable
          products={this.props.products}
          filterText={this.state.filterText}
          inStockOnly={this.state.inStockOnly}
        />
      </div>
    );
  }
}

var PRODUCTS = [
  {category: 'Sporting Goods', price: '$49.99', stocked: true, name: 'Football'},
  {category: 'Sporting Goods', price: '$9.99', stocked: true, name: 'Baseball'},
  {category: 'Sporting Goods', price: '$29.99', stocked: false, name: 'Basketball'},
  {category: 'Electronics', price: '$99.99', stocked: true, name: 'iPod Touch'},
  {category: 'Electronics', price: '$399.99', stocked: false, name: 'iPhone 5'},
  {category: 'Electronics', price: '$199.99', stocked: true, name: 'Nexus 7'}
];

ReactDOM.render(
  <FilterableProductTable products={PRODUCTS} />,
  document.getElementById('container')
);

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


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


Для каждой единицы Состояния вашего приложения:


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

Итак, давайте применим эту стратегию к нашему приложению:


  • Компонент ProductTable нуждается в Состоянии для фильтрации списка продуктов, в то же время Компонент SearchBar нуждается в Состоянии для отображения поискового запроса и состояния чекбокса.
  • Общим предком для них является Компонент FilterableProductTable.
  • Таким образом, концептуально, точка соприкосновения фильтрации списка и выбранных значений находится в Компоненте FilterableProductTable

Отлично, мы определили, что наше Состояние должно быть размещено в Компоненте FilterableProductTable. В первую очердь добавим свойство экземпляра this.state = {filterText: '', inStockOnly: false} в constructor Компонента FilterableProductTable для определения начального Состояния нашего приложения. Затем передадим filterText и inStockOnly в Компоненты ProductTable и SearchBar посредством props. И конечным шагом — используем props для фильтрации строк в ProductTable и установки значений полей формы в SearchBar.


Вы можете запустить приложение и посмотреть как оно будет вести себя: установите значение filterText в конструкторе Компонента FilterableProductTable равным 'ball' и перезагрузите приложение. Вы увидите, что таблица данных корректно обновлена.


Шаг 5: Добавляем обратный поток данных



HTML
<div id="container">
    <!-- Содержание этого элемента будет заменено вашим компонентом. -->
</div>

CSS
body {
    padding: 5px
}

JavaScript
class ProductCategoryRow extends React.Component {
  render() {
    return (<tr><th colSpan="2">{this.props.category}</th></tr>);
  }
}

class ProductRow extends React.Component {
  render() {
    var name = this.props.product.stocked ?
      this.props.product.name :
      <span style={{color: 'red'}}>
        {this.props.product.name}
      </span>;
    return (
      <tr>
        <td>{name}</td>
        <td>{this.props.product.price}</td>
      </tr>
    );
  }
}

class ProductTable extends React.Component {
  render() {
    var rows = [];
    var lastCategory = null;
    this.props.products.forEach((product) => {
      if (product.name.indexOf(this.props.filterText) === -1 || (!product.stocked && this.props.inStockOnly)) {
        return;
      }
      if (product.category !== lastCategory) {
        rows.push(<ProductCategoryRow category={product.category} key={product.category} />);
      }
      rows.push(<ProductRow product={product} key={product.name} />);
      lastCategory = product.category;
    });
    return (
      <table>
        <thead>
          <tr>
            <th>Name</th>
            <th>Price</th>
          </tr>
        </thead>
        <tbody>{rows}</tbody>
      </table>
    );
  }
}

class SearchBar extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
  }

  handleChange() {
    this.props.onUserInput(
      this.filterTextInput.value,
      this.inStockOnlyInput.checked
    );
  }

  render() {
    return (
      <form>
        <input
          type="text"
          placeholder="Search..."
          value={this.props.filterText}
          ref={(input) => this.filterTextInput = input}
          onChange={this.handleChange}
        />
        <p>
          <input
            type="checkbox"
            checked={this.props.inStockOnly}
            ref={(input) => this.inStockOnlyInput = input}
            onChange={this.handleChange}
          />
          {' '}
          Only show products in stock
        </p>
      </form>
    );
  }
}

class FilterableProductTable extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      filterText: '',
      inStockOnly: false
    };

    this.handleUserInput = this.handleUserInput.bind(this);
  }

  handleUserInput(filterText, inStockOnly) {
    this.setState({
      filterText: filterText,
      inStockOnly: inStockOnly
    });
  }

  render() {
    return (
      <div>
        <SearchBar
          filterText={this.state.filterText}
          inStockOnly={this.state.inStockOnly}
          onUserInput={this.handleUserInput}
        />
        <ProductTable
          products={this.props.products}
          filterText={this.state.filterText}
          inStockOnly={this.state.inStockOnly}
        />
      </div>
    );
  }
}

var PRODUCTS = [
  {category: 'Sporting Goods', price: '$49.99', stocked: true, name: 'Football'},
  {category: 'Sporting Goods', price: '$9.99', stocked: true, name: 'Baseball'},
  {category: 'Sporting Goods', price: '$29.99', stocked: false, name: 'Basketball'},
  {category: 'Electronics', price: '$99.99', stocked: true, name: 'iPod Touch'},
  {category: 'Electronics', price: '$399.99', stocked: false, name: 'iPhone 5'},
  {category: 'Electronics', price: '$199.99', stocked: true, name: 'Nexus 7'}
];

ReactDOM.render(
  <FilterableProductTable products={PRODUCTS} />,
  document.getElementById('container')
);

К текущему шагу мы создали приложение, которое корректно передает и использует props и state двигаясь по иерархии Компонентов вниз. Настало время добавит поддержку передачи данных в обратном направлении: компонентам формы, которые расположены ниже по иерархии, необходимо каким-то образом обновлять Состояние в Компоненте FilterableProductTable.


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


Если вы попробуете ввести текст или отметить чекбокс в текущей версии примера, вы обнаружите, что React игнорирует ваш ввод. Это умышленно установлено нами, т.к. мы установили значение свойства value в input всегда эквивалентным state передаваемому из Компонента FilterableProductTable.


Давайте подумаем — что мы хотим чтобы произошло. Мы хотим, чтобы каждый раз, когда пользователь вносит изменения в форму, обновлялось Состояние для отображения пользовательского ввода. Поскольку Компоненты должны обновлять только собственное Состояние, Компоненту FilterableProductTable необходимо передать в Компонент SearchBar механизм обратного вызова, который будет сигнализировать каждый раз, когда Состояние должно быть обновлено. Мы можем использовать событие onChange в компонентах формы для сообщения об этом. Тогда, обратный вызов переданный Компонентом FilterableProductTable вызовет setState() и приложение будет обновлено.


Хотя это и выглядит сложным, но на самом деле это всего лишь несколько строк кода. И реально прозрачно видно как ваши данные передаются через все приложение.


На этом все


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


Первоисточник: React — Quick Start — Thinking in React

Поделиться публикацией
Похожие публикации
Ой, у вас баннер убежал!

Ну. И что?
Реклама
Комментарии 16
    0
    При использовании ES2015 стоит использовать let вместо var.
    Да и при построении React компонентов, лучше описывать типы параметров, передающихся в компонент, ведь читаем мы код чаще, чем пишем ;)
      0
      Это всего лишь перевод и исходники оттуда же.
        0
        Мне, кажется, основный посыл в архитектуре приложения React и наличие/отсуствие let/var и валидаторов PropTypes не наносит смыслового ущерба туториалу из раздела «Быстрый старт».
          0
          А мне кажется, что оригинальная статья, да и вообще вся документация по React написана в такой легкой манере, что даже мне, не особо сведущему человеку в английском, легко удавалось прочитать её в оригинале без гугл транслейта.
        +1
        Я так понимаю, что вы данной статьей хотели показать, что вы не ленивый
        Ибо только тут ленивый не писал tutorial про React…
          +1

          Смысл react'a хранить текущий state клиента, но разве это хорошо? Принципы stateless никто не отменял.

            0

            Вас никто не обязывает использовать состояние. Можете писать stateless компоненты, реализуя их чистыми функциями.

              0

              Речь идёт в частности о состоянии самого компонента. Например выпадающий список (DropDown). Как минимум имеет собственное состояние открыто (open). Когда на него кликнули он открывается и показывает содержимое (open:true), пока что-либо не выберут или пока оно не закроется по другому поводу (open:false).

                0
                Да, и, насколько я понимаю react, состояние этих элементов хранит само приложение а не браузер пользователя. Насколько хорошая эта практика?
              0
              Зачем вы даете сначала snippet с JSFiddle, а потом еще раз то же самое под катом?
                +1
                Для того, чтобы код остался на Хабре. Код в сниппетах имеет свойство умирать за сроком давности или подобным причинам.
                +2
                А расскажите, пожалуйста, что делает вот эта строчка?
                ref={(input) => this.filterTextInput = input}
                
                  0
                  ref — ссылка на ваш элемент.
                  В данном случае, при рендере объекта переменной this.filterTextInput будет присвоен этот ReactElement
                    0

                    Этот ref используется для получения ссылки на наш элемент. React вызывает функцию обработчик прописанную в ref передеавая ей DOM элемент когда компонент монтируется и null когда компонент демонтируется. Документация рекомендует избегать использования этой возможности. Собственно что конкретно делается уже ответили (в поле filterTextInput сохранили ссылку для последующего использования).

                    +3
                    Мне одному кажется, или тот, что фиолетовый, на самом деле голубой?
                      0
                      Поправил. На самом деле там бирюзовый. На автомате написал фиолетовый.

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

                    Самое читаемое