← Назад к вопросам

Как работаешь с запросами внутри компонента?

1.8 Middle🔥 201 комментариев
#React

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI3 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Как работаешь с запросами внутри компонента?

Паттерны для API запросов в React

Работа с API запросами в React компонентах — это одна из самых частых задач. Неправильный подход приводит к race conditions, утечкам памяти, дублирующимся запросам. Я следую конкретной стратегии, которая решает эти проблемы.

Правило 1: Fetch запросы в useEffect — никогда не в render функции и не в обработчиках событий напрямую. useEffect гарантирует, что запрос выполнится в нужный момент жизни компонента.

Правило 2: Cleanup функция в useEffect — необходимо отменять запросы, когда компонент размонтируется. Иначе получишь ошибку "Can't perform a React state update on an unmounted component".

Правило 3: Зависимости в dependency array — важно правильно указать зависимости, чтобы запрос переповторялся, когда меняются параметры.

Правило 4: Разделение ответственности — API логику лучше выносить в кастомные хуки или сервисы, не держать в компоненте.

Практические примеры

// Способ 1: Базовый fetch с useEffect (НЕПРАВИЛЬНО)
// Проблема: race condition, утечка памяти
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    // ПРОБЛЕМА: если userId изменится быстро,
    // выполнится 2 запроса, но разрешиться в обратном порядке
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(data => setUser(data))
      .catch(err => setError(err))
      .finally(() => setLoading(false));
  }, [userId]); // Зависимость указана, но нет cleanup

  return <div>{loading ? "Loading..." : user?.name}</div>;
}

// Способ 2: Правильно с cleanup (AbortController)
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const controller = new AbortController();
    let isMounted = true;

    const fetchUser = async () => {
      try {
        setLoading(true);
        const response = await fetch(`/api/users/${userId}`, {
          signal: controller.signal // Отмена запроса при размонтировании
        });
        
        if (!response.ok) throw new Error("Failed to fetch");
        
        const data = await response.json();
        
        // Проверяем что компонент ещё смонтирован
        if (isMounted) {
          setUser(data);
          setError(null);
        }
      } catch (err) {
        if (err.name !== "AbortError" && isMounted) {
          setError(err.message);
        }
      } finally {
        if (isMounted) {
          setLoading(false);
        }
      }
    };

    fetchUser();

    // Cleanup: отмена запроса и флаг
    return () => {
      isMounted = false;
      controller.abort();
    };
  }, [userId]);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  return <div>{user?.name}</div>;
}

// Способ 3: Кастомный хук для переиспользования
function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    if (!url) {
      setLoading(false);
      return;
    }

    const controller = new AbortController();
    let isMounted = true;

    const fetchData = async () => {
      try {
        const response = await fetch(url, { signal: controller.signal });
        if (!response.ok) throw new Error(`HTTP ${response.status}`);
        
        const result = await response.json();
        if (isMounted) {
          setData(result);
          setError(null);
        }
      } catch (err) {
        if (err.name !== "AbortError" && isMounted) {
          setError(err.message);
        }
      } finally {
        if (isMounted) {
          setLoading(false);
        }
      }
    };

    fetchData();

    return () => {
      isMounted = false;
      controller.abort();
    };
  }, [url]);

  return { data, loading, error };
}

// Использование хука
function UsersList() {
  const { data: users, loading, error } = useFetch("/api/users");
  
  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  return <ul>{users?.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}

// Способ 4: React Query (лучший вариант для production)
import { useQuery } from "@tanstack/react-query";

function UsersList() {
  const { data: users, isLoading, error } = useQuery({
    queryKey: ["users"], // Ключ кэширования
    queryFn: async () => {
      const res = await fetch("/api/users");
      if (!res.ok) throw new Error("Failed to fetch");
      return res.json();
    },
    staleTime: 5 * 60 * 1000, // 5 минут
    gcTime: 10 * 60 * 1000,    // 10 минут
  });

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  return <ul>{users?.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}

// Способ 5: Обработка POST запросов
function CreateUserForm() {
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  const handleSubmit = async (e) => {
    e.preventDefault();
    setLoading(true);
    setError(null);

    try {
      const formData = new FormData(e.target);
      const response = await fetch("/api/users", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(Object.fromEntries(formData))
      });

      if (!response.ok) {
        const errorData = await response.json();
        throw new Error(errorData.message || "Failed to create user");
      }

      const newUser = await response.json();
      // Обновляем состояние или кэш (если используешь React Query)
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input name="name" required />
      <button disabled={loading}>{loading ? "Creating..." : "Create"}</button>
      {error && <div className="error">{error}</div>}
    </form>
  );
}

// Способ 6: Оптимизация с useMemo (для зависимостей)
function UserProfile({ userId, options = {} }) {
  // Мемоизируем объект параметров, чтобы не создавать новый на каждый render
  const memoizedUrl = useMemo(
    () => `/api/users/${userId}?${new URLSearchParams(options)}`,
    [userId, options]
  );

  const { data: user, loading } = useFetch(memoizedUrl);
  return <div>{loading ? "Loading..." : user?.name}</div>;
}

Лучшие практики

  1. Используй React Query для управления сервер-стейтом в production приложениях
  2. Отменяй запросы при размонтировании компонента (AbortController)
  3. Проверяй isMounted перед setState чтобы избежать утечек памяти
  4. Правильно указывай dependency array в useEffect
  5. Кэшируй запросы чтобы избежать дублирования
  6. Обрабатывай ошибки на каждом уровне (network, API, state update)