[Из песочницы] Мыслим в стиле 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

CSS
body {
    padding: 5px
}

JavaScript
class ProductCategoryRow extends React.Component {
  render() {
    return {this.props.category};
  }
}

class ProductRow extends React.Component {
  render() {
    var name = this.props.product.stocked ?
      this.props.product.name :
      
        {this.props.product.name}
      ;
    return (
      
        {name}
        {this.props.product.price}
      
    );
  }
}

class ProductTable extends React.Component {
  render() {
    var rows = [];
    var lastCategory = null;
    this.props.products.forEach(function(product) {
      if (product.category !== lastCategory) {
        rows.push();
      }
      rows.push();
      lastCategory = product.category;
    });
    return (
      {rows}
Name Price
); } } class SearchBar extends React.Component { render() { return (

{' '} Only show products in stock

); } } class FilterableProductTable extends React.Component { render() { return (
); } } 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( , 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

CSS
body {
    padding: 5px
}

JavaScript
class ProductCategoryRow extends React.Component {
  render() {
    return ({this.props.category});
  }
}

class ProductRow extends React.Component {
  render() {
    var name = this.props.product.stocked ?
      this.props.product.name :
      
        {this.props.product.name}
      ;
    return (
      
        {name}
        {this.props.product.price}
      
    );
  }
}

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();
      }
      rows.push();
      lastCategory = product.category;
    });
    return (
      {rows}
Name Price
); } } class SearchBar extends React.Component { render() { return (

{' '} Only show products in stock

); } } class FilterableProductTable extends React.Component { constructor(props) { super(props); this.state = { filterText: '', inStockOnly: false }; } render() { return (
); } } 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( , 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

CSS
body {
    padding: 5px
}

JavaScript
class ProductCategoryRow extends React.Component {
  render() {
    return ({this.props.category});
  }
}

class ProductRow extends React.Component {
  render() {
    var name = this.props.product.stocked ?
      this.props.product.name :
      
        {this.props.product.name}
      ;
    return (
      
        {name}
        {this.props.product.price}
      
    );
  }
}

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();
      }
      rows.push();
      lastCategory = product.category;
    });
    return (
      {rows}
Name Price
); } } 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 (
this.filterTextInput = input} onChange={this.handleChange} />

this.inStockOnlyInput = input} onChange={this.handleChange} /> {' '} Only show products in stock

); } } 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 (
); } } 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( , document.getElementById('container') );

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


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


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


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


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


На этом все


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


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

Комментарии (0)

© Habrahabr.ru