EvolCode
треки/Веб от нуля до сайта·04 / 11
7 мин чтения

Формы в React и список с фильтрацией

В предыдущих уроках разобрали useState и useEffect отдельно. Теперь соберём вместе типичный сценарий — форма-поиск, который фильтрует список товаров. Это ~80% любого React-приложения.

Controlled forms

Стандартный паттерн: на каждом поле есть value (привязан к state) и onChange (обновляет state). Это даёт полный контроль: можно валидировать, очищать, форматировать в реальном времени. Альтернатива — uncontrolled (без value) — оставь для простых форм где не нужна реактивность.

Submit и preventDefault

Форма по умолчанию шлёт данные через перезагрузку страницы. В React это никогда не нужно — мы сами шлём через fetch. Поэтому ВСЕГДА:

$ <form onSubmit={(e) => { e.preventDefault(); ... }}>

preventDefault блокирует дефолтное поведение. Внутри handler можно валидировать, отправлять fetch, обновлять state.

Динамический список через filter

Когда есть массив items и поле поиска q — фильтруешь на лету:

$ items.filter(i => i.name.toLowerCase().includes(q.toLowerCase()))

Это вычисляется при КАЖДОМ рендере — для маленьких списков (до 1000 элементов) бесплатно. Для большИх — useMemo для оптимизации.

Ключи в списке

При .map(items => <Item ... />) каждому ребёнку нужен key. Лучшие ключи — стабильные ID из данных (item.id). Худшие — индекс массива (i): если массив меняется, индексы съезжают и React путается. Если у элементов нет ID — попробуй добавить (Date.now() при создании или uuid).

примерtsx
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>
  );
}

Каталог товаров с поиском, сортировкой и формой добавления

главное
  • 01Controlled inputs: value + onChange — стандарт для форм в React
  • 02onSubmit={(e) => { e.preventDefault(); ... }} — обязательно в формах
  • 03filter + sort + map = сила: пишешь декларативно, React показывает
  • 04key=id (стабильный) НЕ index — иначе при изменении списка React путается
  • 05Иммутабельные обновления: spread, filter, map — никогда mutate
  • 06Date.now() — простейший id для тестов; для прода — uuid или БД-id
  • 07Условный рендер пустого состояния (filtered.length === 0) — UX-маленькая деталь
проверь себя
01У вас в списке элементы без id. Использовать индекс как key?
02Зачем e.preventDefault() в onSubmit?
03Как изменить ОДИН элемент массива в state?
← к треку