← Назад к вопросам
Как организуешь архитектуру загрузки и отображения данных по клику на кнопку?
2.0 Middle🔥 111 комментариев
#JavaScript Core
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI3 апр. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Архитектура загрузки и отображения данных по клику
Это один из самых частых паттернов в веб-приложениях. Правильная архитектура обеспечивает надёжность, масштабируемость и хороший UX.
1. Базовый паттерн с управлением состоянием
import { useState } from 'react'
export function DataLoader() {
const [data, setData] = useState(null)
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState(null)
const handleLoadClick = async () => {
setIsLoading(true)
setError(null)
try {
const response = await fetch('/api/data')
if (!response.ok) throw new Error(`API error: ${response.status}`)
const result = await response.json()
setData(result)
} catch (err) {
setError(err.message)
} finally {
setIsLoading(false)
}
}
return (
<div>
<button onClick={handleLoadClick} disabled={isLoading}>
{isLoading ? 'Загрузка...' : 'Загрузить данные'}
</button>
{error && <div className="error">{error}</div>}
{data && <DataDisplay data={data} />}
</div>
)
}
function DataDisplay({ data }) {
return (
<div className="data-container">
<h2>Загруженные данные</h2>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
)
}
2. Выделение логики в отдельный хук
Если логика загрузки используется в нескольких местах, создаём кастомный хук:
function useDataLoader(url) {
const [data, setData] = useState(null)
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState(null)
const load = async () => {
setIsLoading(true)
setError(null)
try {
const response = await fetch(url)
if (!response.ok) throw new Error(`API error: ${response.status}`)
const result = await response.json()
setData(result)
} catch (err) {
setError(err.message)
} finally {
setIsLoading(false)
}
}
return { data, isLoading, error, load }
}
// Использование
export function DataLoader() {
const { data, isLoading, error, load } = useDataLoader('/api/data')
return (
<div>
<button onClick={load} disabled={isLoading}>
{isLoading ? 'Загрузка...' : 'Загрузить'}
</button>
{error && <ErrorMessage message={error} />}
{data && <DataDisplay data={data} />}
</div>
)
}
3. Архитектура сервиса для работы с API
// services/api.ts
const API_URL = process.env.REACT_APP_API_URL
export const dataService = {
async fetchData(id?: string) {
const url = id ? `${API_URL}/data/${id}` : `${API_URL}/data`
const response = await fetch(url)
if (!response.ok) throw new Error(`API error: ${response.status}`)
return response.json()
},
async fetchUsers() {
return this.fetchData('/users')
},
async fetchUserById(id: string) {
return this.fetchData(`/users/${id}`)
},
}
// Использование в компоненте
function DataLoader() {
const { data, isLoading, error, load } = useDataLoader(() =>
dataService.fetchData()
)
return (...)
}
4. Состояния загрузки с более детальным контролем
function useAsyncData(asyncFn) {
const [state, setState] = useState({
status: 'idle', // idle, loading, success, error
data: null,
error: null
})
const load = async (...args) => {
setState({ status: 'loading', data: null, error: null })
try {
const result = await asyncFn(...args)
setState({ status: 'success', data: result, error: null })
} catch (err) {
setState({ status: 'error', data: null, error: err })
}
}
return { ...state, load }
}
// Компонент с использованием
function DataLoader() {
const { status, data, error, load } = useAsyncData(dataService.fetchData)
return (
<div>
<button onClick={load} disabled={status === 'loading'}>
Загрузить
</button>
{status === 'loading' && <Spinner />}
{status === 'error' && <ErrorBoundary error={error} />}
{status === 'success' && <DataDisplay data={data} />}
</div>
)
}
5. Отмена запросов при размонтировании
function useDataLoader(url) {
const [data, setData] = useState(null)
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState(null)
const load = async () => {
const controller = new AbortController()
setIsLoading(true)
setError(null)
try {
const response = await fetch(url, {
signal: controller.signal
})
if (!response.ok) throw new Error(`API error: ${response.status}`)
const result = await response.json()
setData(result)
} catch (err) {
if (err.name !== 'AbortError') {
setError(err.message)
}
} finally {
setIsLoading(false)
}
// Возвращаем функцию отмены
return () => controller.abort()
}
useEffect(() => {
let abort
const loadData = async () => {
abort = await load()
}
return () => {
if (abort) abort()
}
}, []) // Зависимости
return { data, isLoading, error, load }
}
6. Паттерн с React Query (TanStack Query)
Для сложных приложений используется специализированная библиотека:
import { useQuery, useMutation } from '@tanstack/react-query'
function DataLoader() {
const { data, isLoading, error, refetch } = useQuery({
queryKey: ['data'],
queryFn: () => fetch('/api/data').then(r => r.json()),
enabled: false // Не загружать автоматически
})
return (
<div>
<button onClick={() => refetch()} disabled={isLoading}>
{isLoading ? 'Загрузка...' : 'Загрузить'}
</button>
{error && <ErrorMessage message={error.message} />}
{data && <DataDisplay data={data} />}
</div>
)
}
Преимущества React Query:
- Встроенное кеширование
- Автоматическая переупаковка при потере фокуса
- DevTools для отладки
- Пагинация и infinite queries
7. Оптимистичное обновление UI
function useOptimisticUpdate() {
const [data, setData] = useState(initialData)
const [isPending, setIsPending] = useState(false)
const update = async (newData) => {
// Сразу обновляем UI
const previousData = data
setData(newData)
setIsPending(true)
try {
await fetch('/api/data', {
method: 'POST',
body: JSON.stringify(newData)
})
} catch (err) {
// При ошибке откатываем
setData(previousData)
console.error('Update failed:', err)
} finally {
setIsPending(false)
}
}
return { data, isPending, update }
}
8. Обработка ошибок с retry механикой
function useDataLoaderWithRetry(url, maxRetries = 3) {
const [data, setData] = useState(null)
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState(null)
const [retries, setRetries] = useState(0)
const load = async () => {
setIsLoading(true)
setError(null)
try {
const response = await fetch(url)
if (!response.ok) throw new Error(`HTTP ${response.status}`)
const result = await response.json()
setData(result)
setRetries(0)
} catch (err) {
if (retries < maxRetries) {
// Retry с экспоненциальной задержкой
const delay = Math.pow(2, retries) * 1000
setTimeout(() => {
setRetries(prev => prev + 1)
load()
}, delay)
} else {
setError(`Ошибка после ${maxRetries} попыток: ${err.message}`)
}
} finally {
setIsLoading(false)
}
}
return { data, isLoading, error, retries, load }
}
9. Практический пример: список с фильтрацией
function UsersList() {
const [filter, setFilter] = useState('')
const [page, setPage] = useState(1)
const { data, isLoading, error, load } = useDataLoader(
`/api/users?filter=${filter}&page=${page}`
)
const handleLoadMore = () => {
setPage(prev => prev + 1)
load()
}
const handleFilterChange = (newFilter) => {
setFilter(newFilter)
setPage(1) // Сбрасываем на первую страницу
load()
}
return (
<div>
<SearchInput value={filter} onChange={handleFilterChange} />
<button onClick={load} disabled={isLoading}>
{isLoading ? 'Загрузка...' : 'Загрузить'}
</button>
{error && <ErrorMessage message={error} />}
{data && <UsersList users={data.users} />}
{data?.hasMore && (
<button onClick={handleLoadMore}>Загрузить ещё</button>
)}
</div>
)
}
Лучшие практики
- Разделяй логику и представление - используй хуки для бизнес-логики
- Обрабатывай все состояния - loading, success, error, idle
- Показывай feedback пользователю - спиннер, сообщение об ошибке
- Отменяй запросы при размонтировании компонента
- Используй AbortController для стандартного fetch
- Реализуй retry логику для ненадёжных сетей
- Кешируй данные если нужно избежать повторных запросов
- Для сложных сценариев рассмотри React Query или SWR
Правильная архитектура загрузки данных - это основа хорошего пользовательского опыта и надёжного приложения.