Сегодня хочу поделиться с вами простым, но часто необходимым при работе приёмом: созданием и использованием кастомного хука
useFetch для загрузки данных. Часто в React-компонентах мы дублируем один и тот же код: ставим загрузку, устанавливаем состояние для
data,
error и
loading, пишем
useEffect, чтобы делать вызов API, очищаем эффекты… Всё это можно обобщить в одном месте и переиспользовать во множестве компонент.
Вот базовая реализация хука
useFetch:
import { useState, useEffect, useRef } from 'react';
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
// Чтобы избежать обновления стейта после размонтирования компонента
const isMounted = useRef(true);
useEffect(() => {
// При монтировании флага меняются
isMounted.current = true;
// Начинаем загрузку
fetch(url)
.then(response => {
if (!response.ok) {
throw new Error(`Ошибка ${response.status}`);
}
return response.json();
})
.then(json => {
if (isMounted.current) {
setData(json);
setLoading(false);
}
})
.catch(err => {
if (isMounted.current) {
setError(err.message);
setLoading(false);
}
});
// Очистка: помечаем, что компонент размонтирован
return () => {
isMounted.current = false;
};
}, [url]);
return { data, loading, error };
}
Разбор ключевых моментов:1️⃣
useRef для флага isMountedЕсли компонент размонтируется до того, как придёт ответ от сервера, вызов
setState внутри промиса может вызвать утечку памяти и предупреждение React. Флаг
isMounted.current помогает проверить, что компонент ещё жив.
2️⃣
Состояния data, loading, errorВынесли все три состояния в хук — теперь в компоненте не нужно повторять одно и то же. Достаточно будет написать:
const { data, loading, error } = useFetch('https://api.example.com/posts');
3️⃣
Параметр url в массиве зависимостейЕсли
url меняется, хук автоматически запустит новую загрузку.
4️⃣
Обработка ошибокМы сразу проверяем
response.ok, иначе бросаем исключение, и в
.catch устанавливаем
error.
Теперь пример использования хука в компоненте:
import React from 'react';
import useFetch from './hooks/useFetch';
function PostsList() {
const { data: posts, loading, error } = useFetch('https://jsonplaceholder.typicode.com/posts');
if (loading) {
return <div>Загрузка...</div>;
}
if (error) {
return <div>Ошибка: {error}</div>;
}
return (
<div>
<h2>Список постов</h2>
<ul>
{posts.map(post => (
<li key={post.id}>
<strong>{post.title}</strong>
<p>{post.body}</p>
</li>
))}
</ul>
</div>
);
}
export default PostsList;
Плюсы такого подхода:*
Меньше дублирования кода. Вместо того чтобы копипастить один и тот же
useEffect в десятке компонентов, просто импортируем
useFetch.
*
Централизованная логика. Если понадобится добавить, скажем, кеширование или отмену запроса через AbortController, меняем только внутри
useFetch.
*
Чистый код в компонентах. Компонент сосредоточен на отображении, а все детали работы с сетью спрятаны в хук.
Советы по улучшению:* Можно расширить хук, чтобы принимать не только
url, но и опции
fetch (метод, заголовки и т.п.).
* Добавить параметр
deps (зависимости), чтобы перезапускать запрос не только при изменении URL, а при любой другой переменной.
* Использовать
AbortController, чтобы отменять запросы при новых вызовах или при размонтировании: