Модуль 11: React + TypeScript
Урок 11.6: Типизация API-запросов в React
1. Основы работы с API в React
Работа с API — это одна из ключевых задач современных веб-приложений. При использовании React с TypeScript правильная типизация API-запросов обеспечивает безопасность данных, упрощает отладку и улучшает опыт разработки.
В этом уроке мы подробно разберём все аспекты типизации API-запросов: от определения интерфейсов для ответов сервера до создания типизированных хуков для работы с данными.
Типы HTTP-ответов
Прежде чем мы начнём работать с API, давайте определим базовые типы для HTTP-ответов:
// Базовые типы для API-ответов
interface ApiResponse<T> { data: T; status: number; message?: string; timestamp: string; } interface ApiError { status: number; message: string; errors?: Record<string, string[]>; timestamp: string; } interface PaginatedResponse<T> { data: T[]; pagination: { page: number; limit: number; total: number; totalPages: number; }; } type RequestMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'; interface RequestConfig { method?: RequestMethod; headers?: Record<string, string>; body?: any; signal?: AbortSignal; }
2. Типизация моделей данных
Перед началом работы с API необходимо определить интерфейсы для всех моделей данных, с которыми будет работать ваше приложение.
// Определяем интерфейсы для моделей данных
interface User { id: number; name: string; email: string; avatar?: string; role: 'admin' | 'user' | 'guest'; createdAt: string; updatedAt: string; } interface Post { id: number; title: string; content: string; author: User; tags: string[]; published: boolean; likes: number; comments: Comment[]; createdAt: string; updatedAt: string; } interface Comment { id: number; content: string; author: User; createdAt: string; } interface CreatePostRequest { title: string; content: string; tags?: string[]; published?: boolean; } interface UpdatePostRequest extends Partial<CreatePostRequest> {} interface PostFilters { search?: string; tags?: string[]; authorId?: number; published?: boolean; sortBy?: 'createdAt' | 'updatedAt' | 'likes'; sortOrder?: 'asc' | 'desc'; }
3. Типизация API-клиента
Создадим типизированный API-клиент, который будет автоматически проверять типы ответов сервера.
// Типизированный API-клиент
class ApiClient { private baseUrl: string; private defaultHeaders: Record<string, string>; constructor(baseUrl: string, headers?: Record<string, string>) { this.baseUrl = baseUrl; this.defaultHeaders = { 'Content-Type': 'application/json', ...headers, }; } private async request<T>( endpoint: string, config: RequestConfig = {} ): Promise<T> { const url = `${this.baseUrl}${endpoint}`; const { method = 'GET', headers = {}, body, signal } = config; const response = await fetch(url, { method, headers: { ...this.defaultHeaders, ...headers, }, body: body ? JSON.stringify(body) : undefined, signal, }); if (!response.ok) { const error: ApiError = await response.json(); throw error; } return response.json(); } async get<T>( endpoint: string, params?: Record<string, string | number | boolean> ): Promise<T> { const queryString = params ? '? + Object.entries(params) .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}` ) .join('&') : ''; return this.request<T>(`${endpoint}${queryString}`); } async post<T, B = any>( endpoint: string, body?: B ): Promise<T> { return this.request<T>(endpoint, { method: 'POST', body }); } async put<T, B = any>( endpoint: string, body?: B ): Promise<T> { return this.request<T>(endpoint, { method: 'PUT', body }); } async patch<T, B = any>( endpoint: string, body?: B ): Promise<T> { return this.request<T>(endpoint, { method: 'PATCH', body }); } async delete<T>(endpoint: string): Promise<T> { return this.request<T>(endpoint, { method: 'DELETE' }); } } // Создание экземпляра клиента
const apiClient = new ApiClient('https://api.example.com'); // Типизированные API-вызовы
const fetchUsers = async (): Promise<User[]> => { const response = await apiClient.get<ApiResponse<User[]>>('/users'); return response.data; }; const createPost = async (data: CreatePostRequest): Promise<Post> => { const response = await apiClient.post<ApiResponse<Post>, CreatePostRequest>( '/posts', data ); return response.data; };
4. Кастомные хуки для API
Создадим переиспользуемые хуки для работы с API, которые будут автоматически обрабатывать загрузку, ошибки и кеширование.
// Хук для GET-запросов
interface UseGetResult<T> { data: T | null; loading: boolean; error: string | null; refetch: () => Promise<void>; } function useGet<T>( endpoint: string, params?: Record<string, string | number> ): UseGetResult<T> { const [data, setData] = useState<T | null>(null); const [loading, setLoading] = useState(true); const [error, setError] = useState<string | null>(null); const fetchData = useCallback(async () => { setLoading(true); setError(null); try { const result = await apiClient.get<T>(endpoint, params); setData(result); } catch (err) { setError( err instanceof Error ? err.message : 'Ошибка загрузки данных' ); } finally { setLoading(false); } }, [endpoint, params]); useEffect(() => { fetchData(); }, [fetchData]); return { data, loading, error, refetch: fetchData }; } // Хук для мутаций (POST, PUT, DELETE)
interface UseMutationResult<T, V> { mutate: (variables: V) => Promise<T>; data: T | null; loading: boolean; error: string | null; reset: () => void; } function useMutation<T, V = any>( mutationFn: (variables: V) => Promise<T> ): UseMutationResult<T, V> { const [data, setData] = useState<T | null>(null); const [loading, setLoading] = useState(false); const [error, setError] = useState<string | null>(null); const mutate = useCallback( async (variables: V): Promise<T> => { setLoading(true); setError(null); try { const result = await mutationFn(variables); setData(result); return result; } catch (err) { const errorMessage = err instanceof Error ? err.message : 'Ошибка мутации'; setError(errorMessage); throw err; } finally { setLoading(false); } }, [mutationFn] ); const reset = useCallback(() => { setData(null); setError(null); }, []); return { mutate, data, loading, error, reset }; }
Использование хуков в компонентах
// Компонент списка постов
const PostList = () => { const { data: posts, loading, error, refetch } = useGet<Post[]>('/posts'); if (loading) return <div>Загрузка...</div>; if (error) return <div>Ошибка: {error}</div>; if (!posts) return null; return ( <div> <button onClick={refetch}>Обновить</button> {posts.map(post => ( <article key={post.id}> <h2>{post.title}</h2> <p>{post.content.substring(0, 100)}...</p> <span>Автор: {post.author.name}</span> </article> ))} </div> ); }; // Компонент создания поста
const CreatePostForm = () => { const { mutate: createPost, loading, error } = useMutation<Post, CreatePostRequest>( (data) => apiClient.post<Post>('/posts', data) ); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); await createPost({ title: 'Новый пост', content: 'Содержимое поста', }); }; return ( <form onSubmit={handleSubmit}> <button type="submit" disabled={loading}> {loading ? 'Создание...' : 'Создать пост'} </button> {error && <div className="error">{error}</div>} </form> ); };
5. Типизация с библиотеками
Существует множество библиотек для работы с API в React. Давайте разберём типизацию с наиболее популярными из них.
Axios с TypeScript
// Установка: npm install axios
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios'; // Создание типизированного экземпляра Axios
const api: AxiosInstance = axios.create({ baseURL: 'https://api.example.com', timeout: 10000, headers: { 'Content-Type': 'application/json', }, }); // Типизированные API-вызовы
interface User { id: number; name: string; email: string; } export const userApi = { getAll: async (): Promise<User[]> => { const response = await api.get<User[]>('/users'); return response.data; }, getById: async (id: number): Promise<User> => { const response = await api.get<User>(`/users/${id}`); return response.data; }, create: async (data: Omit<User, 'id'>): Promise<User> => { const response = await api.post<User>('/users', data); return response.data; }, update: async ( id: number, data: Partial<User> ): Promise<User> => { const response = await api.put<User>(`/users/${id}`, data); return response.data; }, delete: async (id: number): Promise<void> => { await api.delete(`/users/${id}`); }, };
6. Типизация WebSocket
Для работы с реалтайм-данными используется WebSocket. Давайте разберём, как типизировать WebSocket-соединения.
// Типы для WebSocket-сообщений
type WsMessageType = 'message' | 'notification' | 'error' | 'ping'; interface WsMessage<T = any> { type: WsMessageType; payload: T; timestamp: string; } interface ChatMessage { id: string; text: string; sender: User; } interface Notification { id: string; title: string; body: string; } // Типизированный хук для WebSocket
function useWebSocket<T>( url: string, onMessage: (message: WsMessage<T>) => void ) { const wsRef = useRef<WebSocket | null>(null); const [isConnected, setIsConnected] = useState(false); useEffect(() => { const ws = new WebSocket(url); wsRef.current = ws; ws.onopen = () => { setIsConnected(true); }; ws.onmessage = (event) => { const message: WsMessage<T> = JSON.parse(event.data); onMessage(message); }; ws.onclose = () => { setIsConnected(false); }; return () => { ws.close(); }; }, [url, onMessage]); const send = useCallback((message: WsMessage) => { if (wsRef.current?.readyState === WebSocket.OPEN) { wsRef.current.send(JSON.stringify(message)); } }, []); return { isConnected, send }; } // Использование:
const Chat = () => { const [messages, setMessages] = useState<ChatMessage[]>([]); const handleMessage = useCallback( (message: WsMessage<ChatMessage>) => { if (message.type === 'message') { setMessages(prev => [...prev, message.payload]); } }, [] ); const { isConnected, send } = useWebSocket<ChatMessage>( 'wss://chat.example.com', handleMessage ); const sendMessage = (text: string) => { send({ type: 'message', payload: { id: Date.now().toString(), text, sender: currentUser }, timestamp: new Date().toISOString(), }); }; return ( ... ); };
7. Типизация GraphQL
GraphQL становится всё более популярным для работы с API. Давайте разберём типизацию с Apollo Client — самой популярной библиотекой для GraphQL в React.
// Установка: npm install @apollo/client graphql
import { gql, useQuery, useMutation } from '@apollo/client'; // Определяем типы для GraphQL-запросов
interface GetPostsData { posts: Post[]; } interface CreatePostData { createPost: Post; } interface CreatePostVariables { title: string; content: string; } // Определяем GraphQL-запросы
const GET_POSTS = gql` query GetPosts { posts { id title content author { name } } } `; const CREATE_POST = gql` mutation CreatePost($title: String!, $content: String!) { createPost(title: $title, content: $content) { id title content } } `; // Использование типизированных хуков
const PostList = () => { const { loading, error, data } = useQuery<GetPostsData>(GET_POSTS); if (loading) return <div>Загрузка...</div>; if (error) return <div>Ошибка: {error.message}</div>; return ( <ul> {data?.posts.map(post => ( <li key={post.id}> <h3>{post.title}</h3> <p>{post.content}</p> <span>Автор: {post.author.name}</span> </li> ))} </ul> ); }; const CreatePostForm = () => { const [createPost, { loading, error }] = useMutation<CreatePostData, CreatePostVariables>( CREATE_POST ); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); await createPost({ variables: { title: 'Новый пост', content: 'Содержимое поста', }, }); }; return ( ... ); };
8. Практическое задание
Выполните следующие задания для закрепления материала:
Задание 1: API-клиент
- Создайте типизированный API-клиент с поддержкой GET, POST, PUT, DELETE
- Добавьте интерцепторы для авторизации
- Реализуйте автоматические повторные попытки при ошибках сети
- Добавьте кеширование ответов
Задание 2: Кастомные хуки
- Создайтеgenerics хук useFetch с поддержкой отмены запросов
- Создайте хук useInfiniteScroll для бесконечной прокрутки
- Создайте хук useDebouncedSearch для отложенного поиска
- Создайте хук usePolling для периодического обновления данных
Задание 3: Приложение с API
- Создайте приложение-список задач с CRUD-операциями
- Используйте типизированный API-клиент
- Добавьте оптимистичные обновления
- Реализуйте обработку ошибок и повторные попытки
9. Итоги урока
В этом уроке мы подробно изучили:
- Определение интерфейсов для моделей данных
- Создание типизированного API-клиента
- Кастомные хуки для работы с API (useGet, useMutation)
- Типизация с библиотеками (Axios, Apollo Client)
- Типизация WebSocket-соединений
- Работа с GraphQL и типизацией
Советы и лучшие практики
- Всегда определяйте интерфейсы для данных, получаемых от API
- Используйтеgenerics для создания переиспользуемых API-клиентов и хуков
- Добавляйте обработку ошибок и состояния загрузки
- Используйте TypeScript для проверки типов ответов API
- Документируйте API-контракты с помощью интерфейсов
Поздравляем! Вы завершили изучение Модуля 11: React + TypeScript. Теперь у вас есть все необходимые знания для создания типизированных React-приложений!
Отличная работа! Вы успешно завершили Модуль 11!
Тест по типизации API-запросов
1 вопрос
Тест по хукам для API
1 вопрос
Тест по Axios с TypeScript
1 вопрос