Свои хуки и загрузка данных
Свои хуки
Хуки 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.