Формы в React и список с фильтрацией
В предыдущих уроках разобрали useState и useEffect отдельно. Теперь соберём вместе типичный сценарий — форма-поиск, который фильтрует список товаров. Это ~80% любого React-приложения.
Controlled forms
Стандартный паттерн: на каждом поле есть value (привязан к state) и onChange (обновляет state). Это даёт полный контроль: можно валидировать, очищать, форматировать в реальном времени. Альтернатива — uncontrolled (без value) — оставь для простых форм где не нужна реактивность.
Submit и preventDefault
Форма по умолчанию шлёт данные через перезагрузку страницы. В React это никогда не нужно — мы сами шлём через fetch. Поэтому ВСЕГДА:
preventDefault блокирует дефолтное поведение. Внутри handler можно валидировать, отправлять fetch, обновлять state.
Динамический список через filter
Когда есть массив items и поле поиска q — фильтруешь на лету:
Это вычисляется при КАЖДОМ рендере — для маленьких списков (до 1000 элементов) бесплатно. Для большИх — useMemo для оптимизации.
Ключи в списке
При .map(items => <Item ... />) каждому ребёнку нужен key. Лучшие ключи — стабильные ID из данных (item.id). Худшие — индекс массива (i): если массив меняется, индексы съезжают и React путается. Если у элементов нет ID — попробуй добавить (Date.now() при создании или uuid).
import { useState } from 'react';
interface Product {
id: number;
name: string;
price: number;
inStock: boolean;
}
const initial: Product[] = [
{ id: 1, name: 'Молоко', price: 95, inStock: true },
{ id: 2, name: 'Хлеб', price: 45, inStock: true },
{ id: 3, name: 'Сыр', price: 280, inStock: false },
{ id: 4, name: 'Йогурт', price: 65, inStock: true },
];
export default function Catalog() {
const [products, setProducts] = useState(initial);
const [query, setQuery] = useState('');
const [showOnlyAvailable, setShowOnlyAvailable] = useState(false);
const [sortBy, setSortBy] = useState<'name' | 'price'>('name');
// Состояние для формы добавления
const [newName, setNewName] = useState('');
const [newPrice, setNewPrice] = useState('');
// Фильтр + сортировка вычисляются при каждом рендере
const filtered = products
.filter((p) => p.name.toLowerCase().includes(query.toLowerCase()))
.filter((p) => (showOnlyAvailable ? p.inStock : true))
.sort((a, b) =>
sortBy === 'price' ? a.price - b.price : a.name.localeCompare(b.name)
);
function addProduct(e: React.FormEvent) {
e.preventDefault(); // не дать форме перезагрузить страницу
if (!newName.trim() || !newPrice) return;
const product = {
id: Date.now(), // простейший id
name: newName,
price: +newPrice,
inStock: true,
};
setProducts([...products, product]); // НОВЫЙ массив
setNewName(''); // очистить форму
setNewPrice('');
}
function removeProduct(id: number) {
setProducts(products.filter((p) => p.id !== id));
}
function toggleStock(id: number) {
setProducts(
products.map((p) => (p.id === id ? { ...p, inStock: !p.inStock } : p))
);
}
return (
<div>
<h1>Каталог</h1>
{/* Фильтры */}
<div className="filters">
<input
placeholder="Поиск..."
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<label>
<input
type="checkbox"
checked={showOnlyAvailable}
onChange={(e) => setShowOnlyAvailable(e.target.checked)}
/>
Только в наличии
</label>
<select value={sortBy} onChange={(e) => setSortBy(e.target.value as any)}>
<option value="name">По имени</option>
<option value="price">По цене</option>
</select>
</div>
{/* Список */}
<ul>
{filtered.map((p) => (
<li key={p.id} style={{ opacity: p.inStock ? 1 : 0.4 }}>
<strong>{p.name}</strong> — {p.price} ₴
<button onClick={() => toggleStock(p.id)}>
{p.inStock ? 'нет в наличии' : 'снова в наличии'}
</button>
<button onClick={() => removeProduct(p.id)}>×</button>
</li>
))}
{filtered.length === 0 && <p>Ничего не нашлось</p>}
</ul>
{/* Форма добавления */}
<form onSubmit={addProduct}>
<input
placeholder="Название"
value={newName}
onChange={(e) => setNewName(e.target.value)}
required
/>
<input
placeholder="Цена"
type="number"
value={newPrice}
onChange={(e) => setNewPrice(e.target.value)}
required
min="0"
/>
<button type="submit">Добавить</button>
</form>
<p>Всего: {filtered.length} из {products.length}</p>
</div>
);
}Каталог товаров с поиском, сортировкой и формой добавления