Перейти к содержимому
Тема 10 из 13
Теория

Свои хуки и загрузка данных

Свои хуки

Хуки useState, useEffect, useRef, useContext — это кирпичики. Из них можно собирать свои хуки. Свой хук — обычная функция, которая:

  • называется с use (useToggle, useWindowWidth, useFetch);
  • внутри вызывает другие хуки.

Зачем? Чтобы вынести логику из компонента и использовать её в нескольких местах. Сравни:

// Было: логика размера окна прямо в компоненте
function Header() {
  const [width, setWidth] = useState(window.innerWidth);
  useEffect(() => {
    const onResize = () => setWidth(window.innerWidth);
    window.addEventListener("resize", onResize);
    return () => window.removeEventListener("resize", onResize);
  }, []);
  return width < 600 ? <MobileMenu /> : <DesktopMenu />;
}
// Стало: логика в хуке, компонент читается с одного взгляда
function useWindowWidth() {
  const [width, setWidth] = useState(window.innerWidth);
  useEffect(() => {
    const onResize = () => setWidth(window.innerWidth);
    window.addEventListener("resize", onResize);
    return () => window.removeEventListener("resize", onResize);
  }, []);
  return width;
}

function Header() {
  const width = useWindowWidth();
  return width < 600 ? <MobileMenu /> : <DesktopMenu />;
}

function Sidebar() {
  const width = useWindowWidth();   // та же логика ещё в одном месте
  return width < 900 ? null : <aside>…</aside>;
}

Важно: хуки делят логику, а не состояние. Каждый вызов useWindowWidth() создаёт своё собственное состояние и свою подписку — как если бы код был вставлен прямо в компонент. Чтобы разные компоненты делили одни данные, нужен контекст (или общий родитель).

Правила хуков действуют и для своих: вызывать только на верхнем уровне компонента или другого хука, не внутри условий и циклов. Имя с use — не просто традиция: по нему React и линтер понимают, что функцию нужно проверять по этим правилам.

Загрузка данных: три состояния

Загрузка с сервера — самый частый эффект. У неё всегда есть три состояния, и показывать нужно каждое:

function ProductList() {
  const [products, setProducts] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    async function load() {
      try {
        const response = await fetch("https://api.example.com/products");
        if (!response.ok) throw new Error(`HTTP ${response.status}`);
        setProducts(await response.json());
      } catch (e) {
        setError(e.message);
      } finally {
        setLoading(false);
      }
    }
    load();
  }, []);

  if (loading) return <p>Загрузка…</p>;
  if (error) return <p className="error">Не удалось загрузить: {error}</p>;
  return (
    <ul>
      {products.map((p) => <li key={p.id}>{p.title}</li>)}
    </ul>
  );
}

Что здесь важно:

  • функцию эффекта нельзя сделать async: useEffect(async () => …) вернёт промис, а React ждёт от эффекта либо ничего, либо функцию очистки. Поэтому внутри объявляют async function load() и сразу вызывают;
  • response.ok проверяем сами: fetch не считает ответ 404 или 500 ошибкой (тема про fetch в курсе JavaScript);
  • finally выключает индикатор загрузки в любом случае.

Загрузка при смене параметров

Если адрес зависит от props или состояния, его указывают в зависимостях — и эффект перезапустится при каждом изменении:

function UserPage({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    setUser(null);                    // сбросить старого, пока грузится новый
    fetch(`https://api.example.com/users/${userId}`)
      .then((r) => r.json())
      .then(setUser);
  }, [userId]);

  if (!user) return <p>Загрузка…</p>;
  return <h1>{user.name}</h1>;
}

Гонка ответов

В этом коде прячется ошибка, которая есть во многих приложениях. Пользователь быстро открыл профиль Ани, а потом Тимура. Ушло два запроса. Ответы приходят в любом порядке: если сервер ответил про Аню позже, чем про Тимура, на странице Тимура окажется Аня.

Решение — функция очистки эффекта. Перед перезапуском эффекта (и при удалении компонента) React вызывает очистку старого эффекта. Отметим в ней, что старый ответ больше не нужен:

useEffect(() => {
  let ignore = false;

  async function load() {
    const response = await fetch(`https://api.example.com/users/${userId}`);
    const data = await response.json();
    if (!ignore) setUser(data);       // старый эффект уже «выключен» — не трогаем состояние
  }
  load();

  return () => {
    ignore = true;
  };
}, [userId]);

У каждого запуска эффекта своя переменная ignore (замыкание!). Когда userId меняется, очистка ставит ignore = true старому запуску, и его ответ, когда бы он ни пришёл, будет пропущен.

Ещё лучше — отменить сам запрос, чтобы не тратить трафик. Для этого у fetch есть AbortController:

useEffect(() => {
  const controller = new AbortController();
  fetch(url, { signal: controller.signal })
    .then((r) => r.json())
    .then(setData)
    .catch((e) => {
      if (e.name !== "AbortError") setError(e.message);
    });
  return () => controller.abort();   // отменяем запрос при смене url или уходе со страницы
}, [url]);

Хук useFetch

Всё вместе отлично укладывается в свой хук — и компоненты становятся совсем простыми:

function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let ignore = false;
    async function load() {
      setLoading(true);
      setError(null);
      try {
        const response = await fetch(url);
        if (!response.ok) throw new Error(`HTTP ${response.status}`);
        const json = await response.json();
        if (!ignore) setData(json);
      } catch (e) {
        if (!ignore) setError(e.message);
      } finally {
        if (!ignore) setLoading(false);
      }
    }
    load();
    return () => {
      ignore = true;
    };
  }, [url]);

  return { data, loading, error };
}

function Weather({ city }) {
  const { data, loading, error } = useFetch(`https://api.example.com/weather?city=${city}`);
  if (loading) return <p>Загрузка…</p>;
  if (error) return <p>Ошибка: {error}</p>;
  return <p>{city}: {data.temp}°</p>;
}

Что используют в больших проектах

Свой useFetch хорош для понимания и небольших приложений. В больших проектах загрузку данных обычно отдают библиотеке TanStack Query (React Query) или SWR: они добавляют кэш (повторное открытие страницы — мгновенно), повторные попытки, обновление при возвращении на вкладку. Устроены они по той же схеме: хук, который возвращает data, isLoading и error.

А фреймворки вроде Next.js умеют загружать данные ещё на сервере, до того как страница пришла в браузер.

Итог

  • Свой хук — функция useЧто-то, которая вызывает другие хуки. Делит логику, а не состояние.
  • Загрузка: состояния loading, data, error; функцию эффекта не делают async — внутри объявляют async function load().
  • Параметры запроса — в зависимостях эффекта.
  • Гонка ответов: let ignore = false + очистка () => { ignore = true }, или AbortController.
  • В больших проектах — TanStack Query или SWR.

Хук useToggle

Лёгкая+10 XP

Напиши свой хук useToggle(initial). Он хранит логическое значение (если initial не передан — false) и возвращает массив [value, toggle], где toggle() меняет значение на противоположное.

function Menu() {
  const [open, toggle] = useToggle();
  return <button onClick={toggle}>{open ? "Close" : "Open"}</button>;
}

Подключи коннектор, чтобы решать задачи

Код задач выполняется на твоём компьютере — так быстрее и надёжнее, чем в браузере. Для этого нужна бесплатная программа-коннектор CodeWithMe. Подключить её нужно один раз.

Ищем коннектор на этом компьютере…

  1. 1
    Скачай коннектор для своей системы (Windows, macOS или Linux).Скачать коннекторНа странице загрузки выбери файл со своей системой в названии, например windows-amd64.
  2. 2Запусти его и выбери папку, где будут лежать твои решения. Python и Node.js он поставит туда сам, когда понадобится.
  3. 3
    Введи ниже код из 6 цифр — он написан в окне коннектора («Код для сайта CodeWithMe»). Если браузер спросит про доступ к локальной сети — разреши.

Коннектор открытый (исходный код на GitHub), работает только на твоём компьютере и выполняет код лишь после ввода кода сопряжения.