Мышление в React


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

Начнем с эскиза


Представьте, что у нас уже есть API в json-формате и эскиз от нашего дизайнера. Наш дизайнер, видимо, не очень хорош, поэтому макет выглядит так:



API в json-формате возвращает некоторые данные, которые выглядит следующим образом:

[
  {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” т.к. данная часть отвечает за визуализацию сбора данных. Однако, в случае если данный заголовок стал сложным (т. е. если бы мы должны были добавить возможности для сортировки), он бы, конечно, присвоил себе компонент “ProductTableHeader”.

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

  • FilterableProductTable
  • SearchBar
  • ProductTable
  • ProductCategoryRow
  • ProductRow


Шаг 2: построить статическую версию в React



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')
);



Codepen

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

Для того, чтобы создать статическую версию приложения, которая отображает вашу модель данных, создайте компоненты, которые повторно используют другие компоненты и передают данные посредством props. Props-это способ передачи данных от прямого предка к дочернему элементу. В данном случае не стоит прибегать к концепции программного уровня. Программный уровень относится исключительно к интерактивности, то есть это данные, которые изменяются с течением времени. Так как это статическая версия приложения, это ни к чему.

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

Компонент в верхней части иерархии (“FilterableProductTable”) примет вашу модель данных в качестве свойства prop. Если вы вносите изменения в вашей базовой модели данных и снова вызываете “ReactDOM.render ()”, пользовательский интерфейс будет обновлен. Можно увидеть, как ваш пользовательский интерфейс обновляется и вносит изменения, так как ничего сложного не происходит. Поток данных в React в одну сторону (также называемый “односторонним обязательным”) сохраняет все модульно и быстро. Если вам нужна помощь в выполнении этого шага, обратитесь к React Docs.

Свойства props и программный уровень


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

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



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

Чтобы правильно построить приложение, вам сначала нужно придумать минимальный набор изменяемых программных уровней, которые нужны приложению. Главное, «не повторяйтесь!». Определите минимальную необходимую форму программного уровня и подсчитайте остальное согласно запросу. Например, если вы создаете список дел, просто сохраните массив элементов списка дел; сохранять отдельную переменную программного уровня не нужно. Наоборот, когда вы захотите сгенерировать подсчет задач списка дел, просто обратитесь к массиву элементов списка дел.

Продумайте все единицы данных в приложении. У нас имеются:
  • Первоначальный список продуктов
  • Текст поиска, введенный пользователем
  • Значение флажка
  • Отфильтрованный список товаров


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

  • Передается ли он от предка с помощью props? Если да, то скорее всего это не программный уровень.
  • Изменяется ли он с течением времени? Если да, то скорее всего это не программный уровень.
  • Можно ли вычислить его на основе любого другого программного уровня или свойства props компонента? Если да, то скорее всего это не программный уровень.


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

Таким образом, программным уровнем являются:
  • Текст поиска, введенный пользователем;
  • Значение флажка


Шаг 4: Местонахождение программного уровня



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')
);



Codepen

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

Помните: React работает только в одностороннем данных вниз по иерархии компонентов. Может быть, не сразу понятно, какой компонент должен овладеть программным уровнем. Чтобы понять это, выполните следующее:

Для каждой части программного уровня в приложении:
  • Идентифицируйте каждый компонент, визуализирует на основе данного программного уровня.
  • Найдите общий заглавный компонент (т.е. компонент, который находится выше остальных в иерархии).
  • Или же программный уровень будет принадлежать тому компоненту, который будет находиться выше остальных в иерархии.
  • Если вы не можете найти компонент, который будет содержать программный уровень, создайте новый компонент просто для поддержания программного уровня и добавьте его в иерархию выше общего заглавного компонента.


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


Теперь мы определили, что програмный уровень находится в “FilterableProductTable”. Во-первых, добавьте свойство экземпляра “this.state = {filterText: '', inStockOnly: false}” в конструктор “FilterableProductTable”, чтобы отразить начальный программный уровень вашего приложения. Затем передайте “filterText” и “inStockOnly” к “ProductTable” и “Searchbar” в качестве props. И, наконец, используйте props для фильтрации строк в “ProductTable” и установите значения полей формы в “Searchbar”.

Вы увидите, как будет вести себя приложение: установите “filterText” на «ball» и обновить приложение. Вы увидите, что таблица данных обновляется корректно.

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



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')
);



Codepen

Мы создали приложение, которое отображается корректно т.к. функции props и программный уровень равномерно распределены в иерархии. Теперь нужно перенаправить поток данных в другую сторону: формы-компоненты, сокрытые глубоко в иерархии необходимо должны обновить программный уроевень в “FilterableProductTable”.

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

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

Теперь нужно решить, что должно произойти. Мы хотим убедиться, что всякий раз, когда пользователь изменяет форму, мы обновляем программный уровень, чтобы отразить входные данные пользователя. Поскольку компоненты обновляют только свой собственный программный уровень, “FilterableProductTable” будет передавать обратный вызов к “Searchbar”, которое будет срабатывать каждый раз, когда программный уровень требует обновления. Мы можем использовать событие “OnChange” для входных данных, чтобы быть в курсе. А обратный вызов из “FilterableProductTable” будет вызывать “SetState ()”, и приложение будет обновляться.

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

Вот и все


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