Где лучше писать запросы к серверу в React?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Где писать запросы к серверу в 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
}, []);
}
Вывод
Иерархия рекомендуемых подходов:
- Custom Hooks — для простых сценариев (1-2 компонента)
- Services + Hooks — для средних приложений
- Context API / Redux — для глобального состояния
- React Query / TanStack Query — для production приложений
Главное правило: API запросы должны быть отделены от UI компонентов. Это делает код тестируемым, переиспользуемым и масштабируемым.