Загрузка данных


# Лабораторная работа: хуки (useState, useEffect)

## Входные файлы

Вам выдаётся приложение «Shelf» — личный трекер чтения:

| Файл             | Назначение                                                             |
| ---------------- | ---------------------------------------------------------------------- |
| `reference.html` | Вёрстка, стили и логика одним файлом — рабочая реализация на чистом JS |

\_Чтобы поднять страницу - можете использовать `Go live` в кодспейсе или `live-server` пакет из npm (`npm install -g live-server` чтобы поставить на всю среду разработки, а не в проект)

## Референс архитектуры приложения

_Обратите внимание, что это архитектура приложения, а не проекта._

_Работаем также в папках `components`, `pages`, в компонентах под каждый компонент создаем папку и кладем туда `jsx` и `css`_

Минимально ожидаемое дерево компонентов:

```
App
 └─ ShelfScreen
     └─ BookForm
          ├─ Checkbox
          ├─ Input
          └─ Button
     ├─ FilterChip
     └─ BookList
          └─ BookItem
```

## Часть 1. Настройка проекта

1. Создайте новое React-приложение (Vite+React+JS+ESLint).

## Часть 2. Разбиение на компоненты и хуки

> Для преобразования `HTML` в `JSX` используйте этот [ сервис ](https://transform.tools/html-to-jsx)

Создайте по одному `.jsx`-файлу на каждый компонент из вашего дерева:

2. **`BookForm`** — контролируемый `input` с собственным `useState` для черновика названия. По кнопке «Добавить на полку» или `Enter` вызывает `onAdd(title)` и очищает своё поле. Пустая/пробельная строка не отправляется.
4. **`BookItem`** — одна книга: цветная обложка-заглушка (можно взять цвет по остатку от деления `id` на длину палитры — как в референсе), название, автор, переключатель «Прочитано» (`onToggleRead`) и кнопка удаления (`onDelete`). Не знает, что такое фильтрация — просто рендерит то, что получил.
5. **`BookList`** — принимает уже отфильтрованный массив `books` и рендерит `BookItem` через `.map()` с `key={book.id}`. Сама фильтрацию не делает.
6. **`ShelfScreen`** — держит логику фильтрации (`books.filter(...)` на основе пропа `showOnlyUnread`) и раздаёт колбэки вниз.
10. **`App`** — единственное место, где находятся все `useState`: `books`, `showOnlyUnread`. Передаёт данные и функции-обработчики вниз по дереву, сам ничего не рендерит напрямую, кроме компоновки дочерних компонентов.

**Важно:** обновления `books` должны быть иммутабельными:

- добавление — через спред (`[...books, newBook]`);
- удаление — через `.filter()`;
- переключение `read` у одной книги — через `.map()`, при этом остальные
  книги должны остаться **теми же объектами** (не пересоздавать их).

## Требования к оформлению кода

- Имя компонента и имя файла совпадают и начинаются с заглавной буквы
  (`BookItem.jsx`, а не `bookItem.jsx`).
- Каждый компонент — отдельный файл, экспортируется по умолчанию
  (`export default`).
- Внутри компонентов, принимающих пропсы, используется деструктуризация в
  параметрах функции, а не обращение через `props.что-то` по всему коду.
- Никаких прямых мутаций состояния (`books.push(...)`, `book.read = true`,
  `arr[i] = ...`) — только через `set...` с новым массивом/объектом.
- У каждого элемента списка есть `key`, построенный на основе `id` книги,
  а не на основе индекса массива.
- Значение из `useState` нигде не читается и не пишется напрямую в DOM
  (никаких `document.getElementById(...)` внутри компонентов).

# Как сдавать

- Создайте форк репозитория в вашей организации с названием-этого-репозитория-вашафамилия
- Используя ветку wip сделайте задание
- Зафиксируйте изменения в вашем репозитории
- Захостите работу
- Когда документ будет готов - создайте пул реквест из ветки wip (вашей) на ветку main (тоже вашу) и укажите меня (ktkv419) как reviewer

Не мержите сами коммит, это сделаю я после проверки задания

---

# Памятка

## useState

### Синтаксис

```
const [значение, setЗначение] = useState(начальноеЗначение);
```

- `значение` — то, что читается при рендере.
- `setЗначение` — единственный правильный способ его изменить. Прямое
  присваивание (`значение = ...`) не вызовет повторный рендер и является
  ошибкой.
- Вызов `useState` должен быть на верхнем уровне компонента: не внутри
  `if`, `for` или вложенной функции.

### Базовый пример: переключение состояния (не число, а булево значение)

Похоже на счётчик, но здесь только два состояния — вкл/выкл.

```
const [isOn, setIsOn] = useState(false);

<button onClick={() => setIsOn(prev => !prev)}>
  {isOn ? 'Выключить свет' : 'Включить свет'}
</button>
```

### Функциональное обновление — когда новое значение зависит от старого

Если внутри одного обработчика вызвать `setX(x + 1)` дважды подряд, оба
вызова "увидят" одно и то же старое `x` из замыкания — в сумме получится
`+1`, а не `+2`. Чтобы React брал самое свежее значение, передавайте в
`set...` функцию, а не готовое число:

```
// Плохо: оба вызова используют одно и то же значение score из рендера
setScore(score + 10);
setScore(score + 10);

// Хорошо: каждый вызов получает актуальное значение на момент применения
setScore(prev => prev + 10);
setScore(prev => prev + 10);
```

Это же правило работает и для `setInterval`/`setTimeout` внутри
`useEffect` — там `x` тоже "застревает" на значении из момента запуска
эффекта, если не использовать функциональную форму.

### Обновление массива — без мутации

```
const [playlist, setPlaylist] = useState([]);

// добавить трек
setPlaylist(prev => [...prev, newTrack]);

// удалить трек по id
setPlaylist(prev => prev.filter(track => track.id !== id));

// пометить один трек избранным, остальные не трогать
setPlaylist(prev =>
  prev.map(track => track.id === id ? { ...track, favorite: !track.favorite } : track)
);
```

Правило простое: если структура — массив или объект, `set...` всегда
получает **новый** массив/объект. `push`, `splice`, прямое присваивание
поля (`track.favorite = true`) — так делать нельзя, React не увидит
изменения.

### Обновление объекта-формы

```
const [profile, setProfile] = useState({ name: '', city: '' });

function handleNameChange(e) {
  setProfile(prev => ({ ...prev, name: e.target.value }));
}
```

Забытый спред (`setProfile({ name: e.target.value })` без `...prev`) —
самая частая ошибка: он затирает остальные поля объекта.

### Когда НЕ нужен отдельный useState

Если значение можно посчитать прямо при рендере из уже существующего
state — не заводите под него ещё один `useState`.

```
const [cartItems, setCartItems] = useState([]);

// Не нужно: const [totalCount, setTotalCount] = useState(0);
// Просто считаем при рендере:
const totalCount = cartItems.length;
const totalPrice = cartItems.reduce((sum, item) => sum + item.price, 0);
```

---

## useEffect

### Синтаксис

```
useEffect(() => {
  // код эффекта — выполняется после рендера
  return () => {
    // необязательный cleanup — выполняется перед следующим запуском
    // эффекта и перед размонтированием компонента
  };
}, [зависимости]);
```

### Три формы второго аргумента

| Второй аргумент | Когда срабатывает                                                    |
| --------------- | ---------------------------------------------------------------------- |
| не передан      | после **каждого** рендера                                            |
| `[]`            | один раз, после первого рендера (монтирование)                       |
| `[a, b]`        | после первого рендера и затем каждый раз, когда `a` или `b` меняются |

### Пример: без зависимостей — логирование каждого рендера

```
useEffect(() => {
  console.log('Компонент отрендерился');
});
```

Полезно для отладки, но в реальном коде почти всегда нужен второй
аргумент — иначе эффект будет срабатывать без остановки.

### Пример: пустой массив — что-то один раз при монтировании

```
useEffect(() => {
  document.title = 'Открыто новое окно чата';
}, []);
```

### Пример: с зависимостью — реакция на конкретное изменение

```
useEffect(() => {
  console.log(`Пользователь открыл раздел: ${activeTab}`);
}, [activeTab]);
```

Сработает только когда поменяется именно `activeTab`, а не при любом
другом обновлении state в этом же компоненте.

### Пример: debounce

```
useEffect(() => {
  const timer = setTimeout(() => {
    ...
  }, 400);

  return () => clearTimeout(timer);
}, [query]);
```

## Пример: Поиск по строке (фильтрация списка)

Часто нужно отфильтровать список карточек по тому, что ввёл пользователь в поле поиска. Пропсами тут не обойтись одними — понадобится **state**, потому что значение поля ввода меняется во время работы приложения. Общая логика:

1. Состояние строки поиска (`searchQuery`) хранится в **родительском** компоненте (том, у которого есть доступ ко всему массиву данных) — например, в `App`.
2. Поле `<input>` — **контролируемый компонент**: его значение берётся из state, а изменения приходят через `onChange`.
3. Перед рендерингом список фильтруется методом `.filter()` по введённой строке.
4. В дочерний компонент (`CourseList`) передаётся **уже отфильтрованный массив**, а не исходный — сам список ничего не знает о поиске.

```jsx
import { useState } from "react";
import courses from "./data/courses.json";

function App() {
  const [searchQuery, setSearchQuery] = useState("");

  // Фильтруем массив по названию курса, без учёта регистра
  const filteredCourses = courses.filter((course) =>
    course.title.toLowerCase().includes(searchQuery.toLowerCase())
  );

  return (
    <>
      <Header />
      <SearchBar value={searchQuery} onChange={setSearchQuery} />
      <CourseList courses={filteredCourses} />
      <Footer />
    </>
  );
}
```

Компонент поля поиска получает текущее значение и функцию-колбэк через пропсы и вызывает её при каждом вводе — сам он состояние не хранит:

```jsx
function SearchBar({ value, onChange }) {
  return (
    <input
      type="text"
      placeholder="Найти курс..."
      value={value}
      onChange={(e) => onChange(e.target.value)}
    />
  );
}
```

`CourseList` при этом не меняется вообще — он как получал массив курсов через проп и рендерил его через `.map()`, так и продолжает это делать, не зная, что массив уже отфильтрован:

```jsx
function CourseList({ courses }) {
  if (courses.length === 0) {
    return <p>Курсы не найдены</p>;
  }

  return (
    <div className="courses__grid">
      {courses.map((course) => (
        <CourseCard key={course.id} course={course} />
      ))}
    </div>
  );
}
```

---

## Пример: тудушка (add / update / delete на одном массиве)

Классический учебный пример, который собирает воедино все три операции с массивом в state: добавление через спред, изменение через `.map()`, удаление через `.filter()`. По структуре он очень похож на `Shelf` из задания — задачи вместо книг, `done` вместо `read`.

```jsx
import { useState } from "react";

function App() {
  const [todos, setTodos] = useState([]);
  const [draft, setDraft] = useState("");

  function handleAdd() {
    const title = draft.trim();
    if (title === "") return; // пустую/пробельную строку не добавляем

    const newTodo = {
      id: Date.now(), // простой способ получить уникальный id
      title,
      done: false,
    };

    // ДОБАВЛЕНИЕ — через спред, старые элементы не трогаем
    setTodos((prev) => [...prev, newTodo]);
    setDraft(""); // очищаем поле после добавления
  }

  function handleToggle(id) {
    // ИЗМЕНЕНИЕ — через .map(), пересоздаём только нужный объект,
    // остальные задачи остаются теми же объектами
    setTodos((prev) =>
      prev.map((todo) =>
        todo.id === id ? { ...todo, done: !todo.done } : todo
      )
    );
  }

  function handleDelete(id) {
    // УДАЛЕНИЕ — через .filter(), новый массив без нужного элемента
    setTodos((prev) => prev.filter((todo) => todo.id !== id));
  }

  return (
    <div>
      <input
        value={draft}
        onChange={(e) => setDraft(e.target.value)}
        onKeyDown={(e) => e.key === "Enter" && handleAdd()}
        placeholder="Новая задача..."
      />
      <button onClick={handleAdd}>Добавить</button>

      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>
            <input
              type="checkbox"
              checked={todo.done}
              onChange={() => handleToggle(todo.id)}
            />
            <span>{todo.title}</span>
            <button onClick={() => handleDelete(todo.id)}>Удалить</button>
          </li>
        ))}
      </ul>
    </div>
  );
}
```

Три ключевых момента:

- **Добавление** всегда создаёт новый массив: `[...prev, newTodo]`, а не `prev.push(newTodo)`.
- **Изменение одного элемента** — через `.map()`: находим нужный `id`, для него собираем новый объект (`{ ...todo, done: !todo.done }`), для остальных возвращаем тот же самый объект без изменений (`todo`) — так React может пропустить перерисовку неизменившихся элементов.
- **Удаление** — через `.filter()`: возвращаем массив без элемента с нужным `id`, ничего не мутируя.

## Условный рендер через state

Помимо самих данных, в state часто хранят **что показывать**: пустой список, состояние загрузки, открыта ли модалка и т.д. Рендерить в зависимости от такого state можно несколькими способами.

### Тернарный оператор — выбор одного из двух вариантов

```jsx
{
  todos.length === 0 ? (
    <p>Задач пока нет</p>
  ) : (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id}>{todo.title}</li>
      ))}
    </ul>
  );
}
```

### `&&` — показать элемент, только если условие истинно

Используется, когда для «иначе» ничего рендерить не нужно.

```jsx
const [showCompleted, setShowCompleted] = useState(false);

<button onClick={() => setShowCompleted((prev) => !prev)}>
  {showCompleted ? "Скрыть выполненные" : "Показать выполненные"}
</button>;

{
  showCompleted && (
    <ul>
      {todos
        .filter((todo) => todo.done)
        .map((todo) => (
          <li key={todo.id}>{todo.title}</li>
        ))}
    </ul>
  );
}
```

Осторожно: если слева от `&&` окажется `0` (например, `todos.length && <ul>...`),
React отрендерит на экране `0`, а не «ничего». Поэтому для чисел лучше явно
сравнивать: `todos.length > 0 && ...`.

### Ранний `return` — когда вариантов рендера много

Удобно, когда одно из состояний полностью исключает остальную разметку компонента (например, состояние загрузки).

```jsx
function TodoList({ todos, isLoading }) {
  if (isLoading) {
    return <p>Загрузка...</p>;
  }

  if (todos.length === 0) {
    return <p>Задач пока нет</p>;
  }

  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id}>{todo.title}</li>
      ))}
    </ul>
  );
}
```

Во всех трёх случаях источник правды один — **state** (`todos`, `showCompleted`, `isLoading`). JSX только читает его и решает, что нарисовать; сам JSX никогда не меняет state напрямую.