Идеи программирования на React

React.js

Эта статья является переводом статьи «Мышление в React» на официальном сайте React.В этой статье команда React представила разработчикам, как создать веб-приложение, чтобы заложить основу для создания веб-приложения с использованием React в будущем. Ниже текст.

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

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

Начните с эскизного проекта

Представьте, что у нас уже есть JSON API и эскиз дизайна от дизайнера. Как показано ниже:

Mockup

Данные, возвращаемые 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, поэтому вы обнаружите, что если модель построена правильно, ваш пользовательский интерфейс (и, следовательно, ваша структура компонентов) должен хорошо отображать модель данных. Это связано с тем, что модели пользовательского интерфейса и данных, как правило, следуют одной и той же информационной архитектуре, а это означает, что усилия по разбиению пользовательского интерфейса на компоненты часто тривиальны. Теперь мы разбиваем его на компоненты, которые отображают модель данных следующим образом:

Component diagram

Теперь у нас есть пять компонентов в нашем примере приложения, и мы выделяем курсивом данные, которые каждый компонент представляет, следующим образом:

  1. **FilterableProductTable** (оранжевый):Компонент, содержащий весь пример
  2. **Поисковая панель** (синяя):Получать все пользовательские данные
  3. **Таблица продуктов** (зеленый):Отображение и фильтрация наборов данных на основе пользовательского ввода
  4. **ProductCategoryRow** (изумрудный цвет):Показать заголовок категории
  5. **ProductRow** (красный):Отображение одного элемента данных о продукте в строке

Осторожно, вы найдете это вProductTable, заголовок (включаяимяа такжеценаlabel) не является компонентом. Это вопрос предпочтения с двумя аргументами. В этом примере мы будем использовать его какProductTableчасть компонента как естьProductTableЧасть набора данных, отвечающая за рендеринг. Однако, если этот заголовок становится сложным (например, мы хотим поддерживать сортировку), установите для него значениеProductTableHeaderТакой компонент определенно был бы лучше.

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

  • FilterableProductTable
    • SearchBar
    • ProductTable
      • ProductCategoryRow
      • ProductRow

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

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

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

    return (
      <tr>
        <td>{name}</td>
        <td>{product.price}</td>
      </tr>
    );
  }
}

class ProductTable extends React.Component {
  render() {
    const rows = [];
    let lastCategory = null;
    
    this.props.products.forEach((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>
    );
  }
}


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

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

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

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

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

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

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

Рассмотрим все данные в нашем примере приложения. У нас есть:

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

Давайте посмотрим, в каком состоянии находится каждый из них. Вот три вопроса о каждой части данных:

  1. Он передается из родительского компонента через реквизит? Если это так, то это, вероятно, не государство.
  2. Он остается прежним? Если это так, то это, вероятно, не государство.
  3. Можете ли вы рассчитать его на основе любого другого состояния или реквизита в компоненте? Если да, то это не государство.

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

Итак, в конце концов, наши состояния:

  • Текст поиска, введенный пользователем
  • значение флажка

Шаг 4: Определите, где нужно разместить ваше состояние

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

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

    return (
      <tr>
        <td>{name}</td>
        <td>{product.price}</td>
      </tr>
    );
  }
}

class ProductTable extends React.Component {
  render() {
    const filterText = this.props.filterText;
    const inStockOnly = this.props.inStockOnly;

    const rows = [];
    let lastCategory = null;

    this.props.products.forEach((product) => {
      if (product.name.indexOf(filterText) === -1) {
        return;
      }
      if (inStockOnly && !product.stocked) {
        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() {
    const filterText = this.props.filterText;
    const inStockOnly = this.props.inStockOnly;

    return (
      <form>
        <input
          type="text"
          placeholder="Search..."
          value={filterText} />
        <p>
          <input
            type="checkbox"
            checked={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>
    );
  }
}


const 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}добавить вFilterableProductTableв конструкторе, чтобы отразить начальное состояние приложения. потомfilterTextа такжеinStockOnlyпередано в качестве реквизитаProductTableа такжеSearchBar. Наконец, используйте эти реквизиты для фильтрацииProductTableлиния в , и вSearchBarУстановите значение поля формы в .

Теперь вы можете увидеть поведение вашего приложения: НастройкиfilterTextдля «мяча» и обновите приложение. Вы увидите, что таблица данных была обновлена ​​правильно.

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

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

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

    return (
      <tr>
        <td>{name}</td>
        <td>{product.price}</td>
      </tr>
    );
  }
}

class ProductTable extends React.Component {
  render() {
    const filterText = this.props.filterText;
    const inStockOnly = this.props.inStockOnly;

    const rows = [];
    let lastCategory = null;

    this.props.products.forEach((product) => {
      if (product.name.indexOf(filterText) === -1) {
        return;
      }
      if (inStockOnly && !product.stocked) {
        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.handleFilterTextChange = this.handleFilterTextChange.bind(this);
    this.handleInStockChange = this.handleInStockChange.bind(this);
  }
  
  handleFilterTextChange(e) {
    this.props.onFilterTextChange(e.target.value);
  }
  
  handleInStockChange(e) {
    this.props.onInStockChange(e.target.checked);
  }
  
  render() {
    return (
      <form>
        <input
          type="text"
          placeholder="Search..."
          value={this.props.filterText}
          onChange={this.handleFilterTextChange}
        />
        <p>
          <input
            type="checkbox"
            checked={this.props.inStockOnly}
            onChange={this.handleInStockChange}
          />
          {' '}
          Only show products in stock
        </p>
      </form>
    );
  }
}

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

  handleFilterTextChange(filterText) {
    this.setState({
      filterText: filterText
    });
  }
  
  handleInStockChange(inStockOnly) {
    this.setState({
      inStockOnly: inStockOnly
    })
  }

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


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

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

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

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

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

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

Вот и все

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

Примечание

Содержимое HTML и CSS всех примеров в этой статье выглядит следующим образом:

<div id="container">
    <!-- This element's contents will be replaced with your component. -->
</div>
body {
  padding: 5px
}