Работа с API в SPA
В SPA взаимодействие с сервером происходит через HTTP-запросы из JavaScript — Fetch API или axios — без перезагрузки страницы; сервер возвращает данные (обычно JSON), а не новый HTML.
Зачем нужно
В отличие от MPA, где сервер формирует HTML-страницы, SPA общается с сервером через REST или GraphQL API. Знание паттернов работы с API — обязательная компетенция: правильная обработка ошибок, отмена запросов, аутентификация через токены, централизованный API-клиент. Без этого код быстро превращается в хаос из разрозненных fetch вызовов.
Где используется
- Fetch API — нативный браузерный инструмент, без зависимостей
- Axios — популярная библиотека с interceptors, автоматическим JSON, отменой
- React Query / SWR — для декларативной загрузки с кешированием
- GraphQL клиенты (Apollo) — типизированные запросы к GraphQL API
Fetch API: основные паттерны
// GET запрос с обработкой ошибок
async function fetchUser(userId) {
const response = await fetch(`/api/users/${userId}`, {
headers: {
'Authorization': `Bearer ${getToken}`,
'Content-Type': 'application/json',
},
});
// fetch не выбрасывает ошибку для HTTP 4xx/5xx — проверяем вручную
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
}
// POST запрос с телом
async function createPost(data) {
const response = await fetch('/api/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message);
}
return response.json();
}
// Отмена запроса через AbortController
async function fetchWithAbort(url, signal) {
const response = await fetch(url, { signal });
return response.json();
}
// Использование в компоненте
useEffect(() => {
const controller = new AbortController();
fetchWithAbort(`/api/users/${id}`, controller.signal)
.then(setUser)
.catch(err => {
if (err.name !== 'AbortError') setError(err);
});
return => controller.abort(); // cleanup при смене id или размонтировании
}, [id]);
Централизованный API-клиент
// api/client.js — единая точка для всех запросов
const BASE_URL = process.env.REACT_APP_API_URL;
async function apiRequest(endpoint, options = {}) {
const token = localStorage.getItem('token');
const config = {
...options,
headers: {
'Content-Type': 'application/json',
...(token && { Authorization: `Bearer ${token}` }),
...options.headers,
},
};
const response = await fetch(`${BASE_URL}${endpoint}`, config);
if (response.status === 401) {
// Токен истёк — редирект на логин
localStorage.removeItem('token');
window.location.href = '/login';
return;
}
if (!response.ok) {
const data = await response.json().catch( => ({}));
throw new Error(data.message || `HTTP ${response.status}`);
}
// 204 No Content — нет тела ответа
if (response.status === 204) return null;
return response.json();
}
// Удобные методы
export const api = {
get: (url) => apiRequest(url),
post: (url, data) => apiRequest(url, { method: 'POST', body: JSON.stringify(data) }),
put: (url, data) => apiRequest(url, { method: 'PUT', body: JSON.stringify(data) }),
delete: (url) => apiRequest(url, { method: 'DELETE' }),
};
// Использование
const user = await api.get('/users/42');
const newPost = await api.post('/posts', { title: 'Привет', content: '...' });
Axios: interceptors
import axios from 'axios';
const apiClient = axios.create({
baseURL: process.env.REACT_APP_API_URL,
timeout: 10000,
});
// Request interceptor — добавляет токен к каждому запросу
apiClient.interceptors.request.use(config => {
const token = localStorage.getItem('token');
if (token) config.headers.Authorization = `Bearer ${token}`;
return config;
});
// Response interceptor — обработка ошибок
apiClient.interceptors.response.use(
response => response.data, // автоматически извлекаем data
error => {
if (error.response?.status === 401) {
window.location.href = '/login';
}
return Promise.reject(error);
}
);
Частые ошибки
- Не проверяют
response.ok—fetchне выбрасывает ошибку для 404/500;response.jsonможет вернуть объект ошибки, который выглядит как успех. - Нет AbortController — при быстрой навигации старые запросы обновляют state размонтированного компонента.
- Прямые fetch-вызовы в компонентах — без централизованного клиента обработка токенов и ошибок дублируется в каждом месте.
Связанные темы
- _MOC SPA
- Загрузка данных и loading states
- Кеширование данных на клиенте
- Оптимистичное обновление UI
- Жизненный цикл компонента