- Quick Start
- Installation
- Describing the UI
- Adding Interactivity
- Managing State
- Escape Hatches
GET STARTED
LEARN REACT
Мислення в 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 добре структурований, ви часто бачитиме, що він природно відображається у структурі компонентів вашого інтерфейсу. Це тому, що інтерфейс і моделі даних часто мають однакову інформаційну архітектуру - тобто однакову форму. Розділіть ваш інтерфейс на компоненти, де кожен компонент відповідає одній частині вашої моделі даних.
На цьому екрані є п'ять компонентів:
SearchBar
(синій) отримує вхідні дані користувача.ProductTable
(лавандовий) відображає та фільтрує список відповідно до введених користувачем даних.ProductCategoryRow
(зелений) демонструє заголовок для кожної категорії.ProductRow
(жовтий) показує рядок для кожного товару.Якщо ви подивитеся на ProductTable
(лаванда), то побачите, що заголовок таблиці (який містить мітки "Назва" та "Ціна") не є її власним компонентом. Це справа ваших вподобань, і ви можете піти будь-яким шляхом. У цьому прикладі він є частиною ProductTable
, оскільки з'являється всередині списку ProductTable
. Однак, якщо цей заголовок стане складнішим (наприклад, якщо ви додасте сортування), ви можете перемістити його у власний компонент ProductTableHeader
.
Тепер, коли ви визначили компоненти у макеті, розташуйте їх в ієрархію. Компоненти, які з'являються всередині іншого компонента у макеті, повинні з'являтися як дочірні в ієрархії:
FilterableProductTable
SearchBar
ProductTable
ProductCategoryRow
ProductRow
Крок 2: Створення статичної версії в React
Тепер, коли у вас є ієрархія компонентів, настав час реалізувати ваш застосунок. Найпростіший підхід - створити версію, яка рендерить інтерфейс на основі вашої моделі даних, не додаючи ніякої інтерактивності... поки що! Часто простіше спочатку створити статичну версію, а інтерактивність додати пізніше. Створення статичної версії вимагає багато тексту і не вимагає роздумів, але додавання інтерактивності вимагає багато роздумів і не вимагає багато тексту.
Щоб створити статичну версію вашого застосунку, яка рендерить вашу модель даних, вам потрібно створити компоненти, які повторно використовують інші компоненти і передають дані за допомогою пропсів. Пропси - це спосіб передачі даних від батька до нащадка. (Якщо ви знайомі з концепцією стану, не використовуйте стан взагалі для побудови цієї статичної версії. Стан зарезервовано лише для інтерактивності, тобто даних, які змінюються з часом. Оскільки це статична версія програми, він вам не потрібен).
Ви можете збирати "зверху вниз", починаючи зі складання компонентів, розташованих вище в ієрархії (наприклад, 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} />;
}
body {
padding: 5px
}
label {
display: block;
margin-top: 5px;
margin-bottom: 5px;
}
th {
padding-top: 10px;
}
td {
padding: 2px;
padding-right: 40px;
}
(Якщо цей код виглядає страшним, спочатку пройдіть Швидкий старт!)
Після створення компонентів ви матимете бібліотеку компонентів багаторазового використання, які рендеритимуть вашу модель даних. Оскільки це статичний застосунок, компоненти повертатимуть лише JSX. Компонент на вершині ієрархії (FilterableProductTable
) візьме вашу модель даних як проп. Це називається одностороннім потоком даних, оскільки дані течуть вниз від компонента верхнього рівня до компонентів внизу дерева.
На цьому етапі вам не слід використовувати жодних значень стану. Це буде зроблено на наступному кроці!
Крок 3: Знайти мінімальне, але повне представлення стану інтерфейсу користувача
Щоб зробити інтерфейс користувача інтерактивним, вам потрібно дозволити користувачам змінювати вашу базову модель даних. Для цього ви будете використовувати стан.
Вважайте стан мінімальним набором даних, що змінюються, які має запам'ятати ваша програма. Найважливішим принципом структурування стану є збереження його DRY (Don't Repeat Yourself). З'ясуйте абсолютне мінімальне представлення стану, яке потрібне вашій програмі, і обчислюйте все інше на вимогу. Наприклад, якщо ви створюєте список покупок, ви можете зберігати елементи як масив у стані. Якщо ви хочете також відобразити кількість елементів у списку, не зберігайте кількість елементів як ще одне значення стану, натомість зчитуйте довжину масиву.
Тепер подумайте про всі дані у цьому прикладі програми:
- Оригінальний список продуктів
- Текст пошуку, який ввів користувач
- Значення прапорця .
- Відфільтрований список товарів
Які з них є державними? Визначте ті, які не є:
- Чи залишається вона незмінною з часом? Якщо так, то це не стан.
- Це передано з батьківського через пропси? Якщо так, то це не стан.
- Чи можете ви обчислити його на основі наявного стану або пропсів у вашому компоненті? Якщо так, то це безумовно не є станом!
Те, що залишилося, ймовірно, є станом.
Пройдімося ще раз по одному:
- Початковий список продуктів передано як пропси, тому він не є станом.
- Текст пошуку, схоже, є станом, оскільки він змінюється з часом і не може бути обчислений з нічого.
- Здається, що значення прапорця є станом, оскільки воно змінюється з часом і не може бути вирахувано з нічого.
- Відфільтрований список товарів не є станом, оскільки його можна обчислити взявши вихідний список товарів і відфільтрувавши його відповідно до тексту пошуку та значення прапорця.
Це означає, що лише текст пошуку та значення прапорця є станом! Чудово!
Пропси проти стану
У React є два типи даних "моделі": пропси та стан. Вони дуже різняться між собою:
- Пропси схожі на аргументи, які ви передаєте у функцію. Вони дозволяють батьківському компоненту передавати дані дочірньому компоненту і налаштовувати його вигляд. Наприклад,
Form
може передати пропcolor
доButton
. - Стан - це як пам'ять компонента. Він дозволяє компоненту відстежувати деяку інформацію та змінювати її у відповідь на взаємодії. Наприклад,
Button
може відстежувати станisHovered
.
Пропси та стан - це різні поняття, але вони працюють разом. Батьківський компонент часто зберігає деяку інформацію у стані (щоб мати змогу її змінювати) і передає її вниз дочірнім компонентам як їхні пропси. Нічого страшного, якщо при першому читанні різниця все ще здається нечіткою. Потрібно трохи потренуватися, щоб вона дійсно стала зрозумілою!
Крок 4: Визначте, де має жити ваша держава
Після визначення мінімальних даних про стан вашого застосунку, вам потрібно визначити, який компонент відповідає за зміну цього стану, або володіє станом. Пам'ятайте: React використовує односторонній потік даних, передаючи дані вниз по ієрархії компонентів від батьківського до дочірнього компонента. Може бути не одразу зрозуміло, який компонент повинен володіти яким станом. Це може бути складно, якщо ви не знайомі з цією концепцією, але ви можете розібратися з цим, виконавши наступні кроки!
Для кожного фрагменту стану у вашій програмі:
- Часто можна помістити стан безпосередньо у їхнього спільного батька.
- Ви також можете помістити стан у якийсь компонент вище їхнього спільного батька.
- Якщо ви не можете знайти компонент, у якому є сенс володіти станом, створіть новий компонент виключно для утримання стану і додайте його десь в ієрархії над спільним батьківським компонентом.
На попередньому кроці ви знайшли два елементи стану у цій програмі: текст для пошуку та значення прапорця. У цьому прикладі вони завжди відображаються разом, тому є сенс помістити їх в одне місце.
Тепер давайте розглянемо нашу стратегію для них:
- Визначення компонентів, які використовують стан:
ProductTable
потрібно відфільтрувати список товарів на основі цього стану (текст пошуку та значення прапорця).SearchBar
потрібно відобразити цей стан (текст пошуку та значення прапорця).
- Знайти їхнього спільного батька: Перший спільний для обох компонентів компонент -
FilterableProductTable
. - Вирішіть, де живе держава: Текст фільтру та значення перевірених станів збережемо у
FilterableProductTable
.
Отже, значення стану будуть жити у FilterableProductTable
.
Додайте стан до компонента за допомогою хука useState()
hook. Хуки - це спеціальні функції, які дозволяють вам "зачепитися" за 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} />;
}
body {
padding: 5px
}
label {
display: block;
margin-top: 5px;
margin-bottom: 5px;
}
th {
padding-top: 5px;
}
td {
padding: 2px;
}
Зверніть увагу, що редагування форми ще не працює. У наведеній вище пісочниці є консольна помилка, яка пояснює, чому:
Ви додали проп `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
і встановите з них батьківський стан:
<input
type="text"
value={filterText}
placeholder="Search..."
onChange={(e) => onFilterTextChange(e.target.value)} />
Тепер застосунок повністю працює!
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} />;
}
body {
padding: 5px
}
label {
display: block;
margin-top: 5px;
margin-bottom: 5px;
}
th {
padding: 4px;
}
td {
padding: 2px;
}
Про обробку подій та оновлення стану можна дізнатися у розділі Додавання інтерактивності.
Куди рухатися далі
Це був дуже короткий вступ до того, як думати про створення компонентів та застосунків за допомогою React. Ви можете розпочати React-проект прямо зараз або зануритися глибше у весь синтаксис, використаний у цьому підручнику.