Spec-Zone.ru › React 18

Размышления о React

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

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

Представьте, что у вас уже есть JSON-API и макет от дизайнера.

JSON-API возвращает данные, которые выглядят так:

[
  { category: "Fruits", price: "$1", stocked: true, name: "Apple" },
  { category: "Fruits", price: "$1", stocked: true, name: "Dragonfruit" },
  { category: "Fruits", price: "$2", stocked: false, name: "Passionfruit" },
  { category: "Vegetables", price: "$2", stocked: true, name: "Spinach" },
  { category: "Vegetables", price: "$4", stocked: false, name: "Pumpkin" },
  { category: "Vegetables", price: "$1", stocked: true, name: "Peas" }
]

Макет выглядит следующим образом:

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

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

Начните с обведения каждой области и подобласти в макете рамками и присвоением им имен. Если вы работаете с дизайнером, он, возможно, уже присвоил имена этим компонентам в инструменте для дизайна. Спросите его!

В зависимости от вашего опыта, вы можете по-разному разделить дизайн на компоненты:

  • Программирование—используйте те же методы, что и при решении вопроса о создании новой функции или объекта. Одним из таких методов является принцип единой ответственности, то есть компонент, по идее, должен выполнять только одну задачу. Если он начинает разрастаться, его следует разделить на более мелкие подкомпоненты.
  • CSS—подумайте о том, для чего вы бы создавали селекторы классов. (Однако компоненты немного менее детализированы.)
  • Дизайн—подумайте о том, как вы бы организовали слои дизайна.

Если ваш JSON структурирован хорошо, вы часто обнаружите, что он естественным образом соответствует структуре компонентов вашего пользовательского интерфейса. Это происходит потому, что модели пользовательского интерфейса и данных часто имеют одинаковую архитектуру информации — то есть одинаковую структуру. Разделите ваш пользовательский интерфейс на компоненты, где каждый компонент соответствует одной части вашей модели данных.

На этом экране пять компонентов:

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

Если вы посмотрите на ProductTable (лиловый), вы увидите, что заголовок таблицы (содержащий метки «Название» и «Цена») не является отдельным компонентом. Это вопрос предпочтений, и вы можете выбрать любой вариант. В этом примере он является частью ProductTable, так как появляется внутри списка ProductTable. Однако, если этот заголовок станет сложным (например, если вы добавите сортировку), вы можете перенести его в собственный ProductTableHeader компонент.

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

  • FilterableProductTable
    • SearchBar
    • ProductTable
      • ProductCategoryRow
      • ProductRow

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

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

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

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

function ProductCategoryRow({ category }) {
  return (
    <tr>
      <th colSpan="2">
        {category}
      </th>
    </tr>
  );
}

function ProductRow({ product }) {
  const name = product.stocked ? product.name :
    <span style={{ color: 'red' }}>
      {product.name}
    </span>;

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

function ProductTable({ products }) {
  const rows = [];
  let lastCategory = null;

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

function SearchBar() {
  return (
    <form>
      <input type="text" placeholder="Search..." />
      <label>
        <input type="checkbox" />
        {' '}
        Only show products in stock
      </label>
    </form>
  );
}

function FilterableProductTable({ products }) {
  return (
    <div>
      <SearchBar />
      <ProductTable products={products} />
    </div>
  );
}

const PRODUCTS = [
  {category: "Fruits", price: "$1", stocked: true, name: "Apple"},
  {category: "Fruits", price: "$1", stocked: true, name: "Dragonfruit"},
  {category: "Fruits", price: "$2", stocked: false, name: "Passionfruit"},
  {category: "Vegetables", price: "$2", stocked: true, name: "Spinach"},
  {category: "Vegetables", price: "$4", stocked: false, name: "Pumpkin"},
  {category: "Vegetables", price: "$1", stocked: true, name: "Peas"}
];

export default function App() {
  return <FilterableProductTable products={PRODUCTS} />;
}

(Если этот код кажется сложным, сначала пройдите быстрый старт!)

После создания компонентов у вас будет библиотека переиспользуемых компонентов, отображающих вашу модель данных. Поскольку это статическое приложение, компоненты будут возвращать только JSX. Компонент в верхней части иерархии (FilterableProductTable) будет принимать вашу модель данных как свойство. Это называется односторонним потоком данных, потому что данные поступают сверху вниз от компонента верхнего уровня к компонентам в нижней части дерева.

Опасность

На данном этапе вы не должны использовать значения состояния. Это для следующего шага!

Шаг 3: Найти минимальное, но полное представление состояния пользовательского интерфейса

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

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

Теперь подумайте обо всех данных в этом примере приложения:

  1. Исходный список продуктов
  2. Текст поиска, введенный пользователем
  3. Значение флажка
  4. Отфильтрованный список продуктов

Какие из них являются состоянием? Определите те, которые не являются:

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

Остальное, вероятно, является состоянием.

Давайте пройдемся по ним еще раз:

  1. Исходный список продуктов передан как свойства, поэтому он не является состоянием.
  2. Текст поиска, кажется, является состоянием, поскольку он изменяется со временем и не может быть вычислен из ничего.
  3. Значение флажка, кажется, является состоянием, поскольку оно изменяется со временем и не может быть вычислено из ничего.
  4. Отфильтрованный список продуктов не является состоянием, потому что его можно вычислить, взяв исходный список продуктов и отфильтровав его в соответствии с текстом поиска и значением флажка.

Это означает, что только текст поиска и значение флажка являются состоянием! Хорошо сделано!

Детали

Свойства против состояния

В React есть два типа данных «модели»: свойства и состояние. Они очень разные:

  • Свойства похожи на аргументы, которые вы передаете функции. Они позволяют родительскому компоненту передавать данные дочернему компоненту и настраивать его внешний вид. Например, Form может передать color свойство Button.
  • Состояние — это своего рода память компонента. Оно позволяет компоненту отслеживать некоторую информацию и изменять ее в ответ на взаимодействия. Например, Button может отслеживать isHovered состояние.

Свойства и состояние отличаются, но работают вместе. Родительский компонент часто хранит некоторую информацию в состоянии (чтобы он мог ее изменить) и передает ее дочерним компонентам в качестве их свойств. В порядке вещей, если разница по-прежнему кажется неясной с первого прочтения. Для этого нужно немного практики!

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

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

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

  1. Определите все компоненты, которые отображают что-либо на основе этого состояния.
  2. Найдите их ближайшего общего родительского компонента — компонент, расположенный выше всех в иерархии.
  3. Определите, где должно храниться состояние:
    1. Часто вы можете поместить состояние непосредственно в их общего родителя.
    2. Вы также можете поместить состояние в какой-либо компонент над их общим родителем.
    3. Если вы не можете найти компонент, где имеет смысл владеть состоянием, создайте новый компонент только для хранения состояния и добавьте его где-нибудь в иерархии над общим родительским компонентом.

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

Теперь давайте рассмотрим нашу стратегию для них:

  1. Определите компоненты, использующие состояние:
    • ProductTable должен отфильтровать список продуктов на основе этого состояния (текста поиска и значения флажка).
    • SearchBar должен отобразить это состояние (текст поиска и значение флажка).
  2. Найдите их общего родителя: Первым родительским компонентом, который имеют оба компонента, является FilterableProductTable.
  3. Определите, где хранится состояние: Мы сохраним значения фильтрации текста и checked в FilterableProductTable.

Итак, значения состояния будут храниться в FilterableProductTable.

Добавьте состояние в компонент с помощью useState() крючка. Крючки — это специальные функции, которые позволяют вам «подключиться» к React. Добавьте две переменные состояния в верхней части FilterableProductTable и укажите их начальное состояние:

function FilterableProductTable({ products }) {
  const [filterText, setFilterText] = useState('');
  const [inStockOnly, setInStockOnly] = useState(false);

Затем передайте filterText и inStockOnly в ProductTable и SearchBar как свойства:

<div>
  <SearchBar 
    filterText={filterText} 
    inStockOnly={inStockOnly} />
  <ProductTable 
    products={products}
    filterText={filterText}
    inStockOnly={inStockOnly} />
</div>

Вы можете начать видеть, как будет вести себя ваше приложение. Измените начальное значение filterText с useState('') на useState('fruit') в коде песочницы ниже. Вы увидите, как обновляются как поле ввода поиска, так и таблица:

import { useState } from 'react';

function FilterableProductTable({ products }) {
  const [filterText, setFilterText] = useState('');
  const [inStockOnly, setInStockOnly] = useState(false);

  return (
    <div>
      <SearchBar 
        filterText={filterText} 
        inStockOnly={inStockOnly} />
      <ProductTable 
        products={products}
        filterText={filterText}
        inStockOnly={inStockOnly} />
    </div>
  );
}

function ProductCategoryRow({ category }) {
  return (
    <tr>
      <th colSpan="2">
        {category}
      </th>
    </tr>
  );
}

function ProductRow({ product }) {
  const name = product.stocked ? product.name :
    <span style={{ color: 'red' }}>
      {product.name}
    </span>;

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

function ProductTable({ products, filterText, inStockOnly }) {
  const rows = [];
  let lastCategory = null;

  products.forEach((product) => {
    if (
      product.name.toLowerCase().indexOf(
        filterText.toLowerCase()
      ) === -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>
  );
}

function SearchBar({ filterText, inStockOnly }) {
  return (
    <form>
      <input 
        type="text" 
        value={filterText} 
        placeholder="Search..."/>
      <label>
        <input 
          type="checkbox" 
          checked={inStockOnly} />
        {' '}
        Only show products in stock
      </label>
    </form>
  );
}

const PRODUCTS = [
  {category: "Fruits", price: "$1", stocked: true, name: "Apple"},
  {category: "Fruits", price: "$1", stocked: true, name: "Dragonfruit"},
  {category: "Fruits", price: "$2", stocked: false, name: "Passionfruit"},
  {category: "Vegetables", price: "$2", stocked: true, name: "Spinach"},
  {category: "Vegetables", price: "$4", stocked: false, name: "Pumpkin"},
  {category: "Vegetables", price: "$1", stocked: true, name: "Peas"}
];

export default function App() {
  return <FilterableProductTable products={PRODUCTS} />;
}

Обратите внимание, что редактирование формы пока не работает. В консоли песочницы выше есть ошибка, объясняющая почему:

Консоль
Вы задали свойство `value` для поля формы без обработчика `onChange`. Это приведет к отображению только для чтения.

В песочнице выше, ProductTable и SearchBar считывают свойства filterText и inStockOnly для отображения таблицы, поля ввода и флажка. Например, вот как SearchBar заполняет значение поля ввода:

function SearchBar({ filterText, inStockOnly }) {
  return (
    <form>
      <input 
        type="text" 
        value={filterText} 
        placeholder="Search..."/>

Однако вы еще не добавили код для реагирования на действия пользователя, например, на написание текста. Это будет ваш последний шаг.

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

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

React делает этот поток данных явным, но он требует немного больше кода, чем двунаправленная привязка данных. Если вы попытаетесь что-то набрать или поставить галочку в примере выше, вы увидите, что React игнорирует ваш ввод. Это сделано намеренно. Записав <input value={filterText} />, вы установили свойство value компонента input всегда равным состоянию filterText , переданному из FilterableProductTable . Так как состояние filterText никогда не устанавливается, поле ввода никогда не меняется.

Вы хотите сделать так, чтобы всякий раз, когда пользователь изменяет поля формы, состояние обновлялось в соответствии с этими изменениями. Состояние принадлежит FilterableProductTable, поэтому только он может вызывать setFilterText и setInStockOnly. Чтобы позволить SearchBar обновить состояние FilterableProductTable, вам нужно передать эти функции компоненту SearchBar:

function FilterableProductTable({ products }) {
  const [filterText, setFilterText] = useState('');
  const [inStockOnly, setInStockOnly] = useState(false);

  return (
    <div>
      <SearchBar 
        filterText={filterText} 
        inStockOnly={inStockOnly}
        onFilterTextChange={setFilterText}
        onInStockOnlyChange={setInStockOnly} />

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

function SearchBar({
  filterText,
  inStockOnly,
  onFilterTextChange,
  onInStockOnlyChange
}) {
  return (
    <form>
      <input
        type="text"
        value={filterText}
        placeholder="Search..."
        onChange={(e) => onFilterTextChange(e.target.value)}
      />
      <label>
        <input
          type="checkbox"
          checked={inStockOnly}
          onChange={(e) => onInStockOnlyChange(e.target.checked)}

Теперь приложение полностью работает!

import { useState } from 'react';

function FilterableProductTable({ products }) {
  const [filterText, setFilterText] = useState('');
  const [inStockOnly, setInStockOnly] = useState(false);

  return (
    <div>
      <SearchBar 
        filterText={filterText} 
        inStockOnly={inStockOnly} 
        onFilterTextChange={setFilterText} 
        onInStockOnlyChange={setInStockOnly} />
      <ProductTable 
        products={products} 
        filterText={filterText}
        inStockOnly={inStockOnly} />
    </div>
  );
}

function ProductCategoryRow({ category }) {
  return (
    <tr>
      <th colSpan="2">
        {category}
      </th>
    </tr>
  );
}

function ProductRow({ product }) {
  const name = product.stocked ? product.name :
    <span style={{ color: 'red' }}>
      {product.name}
    </span>;

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

function ProductTable({ products, filterText, inStockOnly }) {
  const rows = [];
  let lastCategory = null;

  products.forEach((product) => {
    if (
      product.name.toLowerCase().indexOf(
        filterText.toLowerCase()
      ) === -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>
  );
}

function SearchBar({
  filterText,
  inStockOnly,
  onFilterTextChange,
  onInStockOnlyChange
}) {
  return (
    <form>
      <input 
        type="text" 
        value={filterText} placeholder="Search..." 
        onChange={(e) => onFilterTextChange(e.target.value)} />
      <label>
        <input 
          type="checkbox" 
          checked={inStockOnly} 
          onChange={(e) => onInStockOnlyChange(e.target.checked)} />
        {' '}
        Only show products in stock
      </label>
    </form>
  );
}

const PRODUCTS = [
  {category: "Fruits", price: "$1", stocked: true, name: "Apple"},
  {category: "Fruits", price: "$1", stocked: true, name: "Dragonfruit"},
  {category: "Fruits", price: "$2", stocked: false, name: "Passionfruit"},
  {category: "Vegetables", price: "$2", stocked: true, name: "Spinach"},
  {category: "Vegetables", price: "$4", stocked: false, name: "Pumpkin"},
  {category: "Vegetables", price: "$1", stocked: true, name: "Peas"}
];

export default function App() {
  return <FilterableProductTable products={PRODUCTS} />;
}

Вы можете узнать все об обработке событий и обновлении состояния в разделе Обработка взаимодействий.

Куда двигаться дальше

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

© 2013–present Facebook Inc.
Licensed under the Creative Commons Attribution 4.0 International Public License.
https://18.react.dev/learn/thinking-in-react

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API