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

Где лучше писать запросы к серверу в React?

2.0 Middle🔥 301 комментариев
#React#Архитектура и паттерны

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

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

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

Где писать запросы к серверу в React?

Это один из самых важных вопросов в React архитектуре. Правильное размещение API запросов влияет на производительность, тестируемость и масштабируемость приложения.

Правило: Не в компонентах

Главный принцип: Компоненты должны отвечать только за рендеринг, а не за логику получения данных.

// Плохо — API прямо в компоненте
export function QuestionList() {
  const [questions, setQuestions] = useState([]);

  useEffect(() => {
    fetch('/api/questions')
      .then(r => r.json())
      .then(data => setQuestions(data))
      .catch(error => console.error(error));
  }, []);

  return <div>{questions.map(q => <div key={q.id}>{q.title}</div>)}</div>;
}

Проблемы:

  • Трудно тестировать (нужно мокировать fetch)
  • Дублирование кода во многих компонентах
  • Смешивание concerns (UI + логика данных)
  • Сложно переиспользовать логику

Правильное место: Custom Hooks

Custom hooks — идеальное место для API логики. Хук инкапсулирует состояние и побочные эффекты.

// hooks/useQuestions.ts
export function useQuestions(categoryId?: string) {
  const [questions, setQuestions] = useState<Question[]>([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const fetchQuestions = async () => {
      setLoading(true);
      setError(null);
      try {
        const params = categoryId ? `?category=${categoryId}` : '';
        const response = await fetch(`/api/questions${params}`);
        if (!response.ok) throw new Error('Failed to fetch');
        const data = await response.json();
        setQuestions(data);
      } catch (err) {
        setError(err instanceof Error ? err.message : 'Unknown error');
      } finally {
        setLoading(false);
      }
    };

    fetchQuestions();
  }, [categoryId]);

  return { questions, loading, error };
}

// component.tsx
export function QuestionList({ categoryId }: { categoryId: string }) {
  const { questions, loading, error } = useQuestions(categoryId);

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

  return <div>{questions.map(q => <div key={q.id}>{q.title}</div>)}</div>;
}

Преимущества:

  • Логика отделена от компонента
  • Легко тестировать (просто мокировать fetch)
  • Переиспользуемо в разных компонентах
  • Понятное API

Слой Services (для более сложных сценариев)

Для больших приложений создай отдельный слой сервисов:

// lib/services/questionService.ts
class QuestionService {
  private baseUrl = '/api/questions';

  async getAll(categoryId?: string): Promise<Question[]> {
    const params = categoryId ? `?category=${categoryId}` : '';
    const response = await fetch(`${this.baseUrl}${params}`);
    if (!response.ok) throw new Error('Failed to fetch questions');
    return response.json();
  }

  async getById(id: string): Promise<Question> {
    const response = await fetch(`${this.baseUrl}/${id}`);
    if (!response.ok) throw new Error('Question not found');
    return response.json();
  }

  async create(data: CreateQuestionDTO): Promise<Question> {
    const response = await fetch(this.baseUrl, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data)
    });
    if (!response.ok) throw new Error('Failed to create question');
    return response.json();
  }

  async update(id: string, data: UpdateQuestionDTO): Promise<Question> {
    const response = await fetch(`${this.baseUrl}/${id}`, {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data)
    });
    if (!response.ok) throw new Error('Failed to update question');
    return response.json();
  }
}

export const questionService = new QuestionService();

Теперь хук просто вызывает сервис:

// hooks/useQuestions.ts
import { questionService } from '@/lib/services/questionService';

export function useQuestions(categoryId?: string) {
  const [questions, setQuestions] = useState<Question[]>([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const load = async () => {
      setLoading(true);
      try {
        const data = await questionService.getAll(categoryId);
        setQuestions(data);
      } catch (err) {
        setError(err instanceof Error ? err.message : 'Error');
      } finally {
        setLoading(false);
      }
    };

    load();
  }, [categoryId]);

  return { questions, loading, error };
}

State Management для сложных случаев

Если данные нужны в разных местах приложения — используй Context API или Redux:

// contexts/QuestionsContext.tsx
interface QuestionsContextType {
  questions: Question[];
  loading: boolean;
  error: string | null;
  fetchQuestions: (categoryId?: string) => Promise<void>;
}

const QuestionsContext = createContext<QuestionsContextType | null>(null);

export function QuestionsProvider({ children }: { children: React.ReactNode }) {
  const [questions, setQuestions] = useState<Question[]>([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const fetchQuestions = async (categoryId?: string) => {
    setLoading(true);
    try {
      const data = await questionService.getAll(categoryId);
      setQuestions(data);
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Error');
    } finally {
      setLoading(false);
    }
  };

  return (
    <QuestionsContext.Provider value={{ questions, loading, error, fetchQuestions }}>
      {children}
    </QuestionsContext.Provider>
  );
}

export function useQuestionsContext() {
  const context = useContext(QuestionsContext);
  if (!context) throw new Error('useQuestionsContext must be used within QuestionsProvider');
  return context;
}

Использование:

export function QuestionList() {
  const { questions, loading, error } = useQuestionsContext();
  return <div>{/* ... */}</div>;
}

React Query / TanStack Query (Best Practice)

Для реальных приложений лучше использовать специализированную библиотеку:

// Установка: npm install @tanstack/react-query

import { useQuery } from '@tanstack/react-query';

export function QuestionList({ categoryId }: { categoryId: string }) {
  const { data: questions, isLoading, error } = useQuery({
    queryKey: ['questions', categoryId],
    queryFn: () => questionService.getAll(categoryId),
    staleTime: 5 * 60 * 1000 // кэш на 5 минут
  });

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return <div>{questions?.map(q => <div key={q.id}>{q.title}</div>)}</div>;
}

Преимущества React Query:

  • Автоматическое кэширование
  • Background refetching
  • Оптимистичные обновления
  • DevTools для отладки
  • Обработка ошибок и retry логика
  • Pagination и infinite scroll из коробки

Структура рекомендуемого проекта

src/
├── lib/
│   ├── api.ts              # Настройки HTTP клиента
│   ├── services/
│   │   ├── questionService.ts
│   │   ├── userService.ts
│   │   └── authService.ts
│   └── utils.ts
├── hooks/
│   ├── useQuestions.ts
│   ├── useUser.ts
│   └── useAuth.ts
├── contexts/              # Если нужен Context API
│   └── QuestionsContext.tsx
├── components/
│   └── QuestionList.tsx
└── types/
    └── index.ts

Практический пример: Полный flow

// lib/api.ts — единая точка конфигурации
const API_BASE = process.env.REACT_APP_API_URL || 'https://api.example.com';

export async function apiCall<T>(
  endpoint: string,
  options?: RequestInit
): Promise<T> {
  const response = await fetch(`${API_BASE}${endpoint}`, {
    ...options,
    headers: {
      'Content-Type': 'application/json',
      ...(options?.headers || {})
    }
  });

  if (!response.ok) {
    throw new Error(`API Error: ${response.status}`);
  }

  return response.json();
}

// lib/services/questionService.ts
export class QuestionService {
  async getAll(categoryId?: string) {
    return apiCall<Question[]>(
      categoryId ? `/questions?category=${categoryId}` : '/questions'
    );
  }
}

// hooks/useQuestions.ts
export function useQuestions(categoryId?: string) {
  const [state, setState] = useState<{
    data: Question[];
    loading: boolean;
    error: string | null;
  }>({
    data: [],
    loading: false,
    error: null
  });

  useEffect(() => {
    const service = new QuestionService();
    let isMounted = true;

    (async () => {
      setState(s => ({ ...s, loading: true }));
      try {
        const data = await service.getAll(categoryId);
        if (isMounted) setState({ data, loading: false, error: null });
      } catch (err) {
        if (isMounted) {
          setState({ data: [], loading: false, error: 'Failed to load' });
        }
      }
    })();

    return () => {
      isMounted = false; // Избегаем утечек памяти
    };
  }, [categoryId]);

  return state;
}

Антипаттерны (чего ИЗБЕГАТЬ)

// 1. Прямой fetch в компоненте
function BadComponent() {
  useEffect(() => {
    fetch('/api/data').then(r => r.json()).then(/* ... */);
  }, []);
  // Плохо: тесты, переиспользование, отладка
}

// 2. API запросы в обработчиках событий без обработки состояния
function BadForm() {
  const handleSubmit = async () => {
    const res = await fetch('/api/submit', { method: 'POST' });
    // Плохо: нет loading, error handling, отладки
  };
}

// 3. Забыть cleanup в useEffect
function BadCleanup() {
  useEffect(() => {
    fetch('/api/data').then(/* ... */); // Утечка памяти если компонент unmount
  }, []);
}

Вывод

Иерархия рекомендуемых подходов:

  1. Custom Hooks — для простых сценариев (1-2 компонента)
  2. Services + Hooks — для средних приложений
  3. Context API / Redux — для глобального состояния
  4. React Query / TanStack Query — для production приложений

Главное правило: API запросы должны быть отделены от UI компонентов. Это делает код тестируемым, переиспользуемым и масштабируемым.