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

Как организуешь архитектуру загрузки и отображения данных по клику на кнопку?

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

Правильная архитектура загрузки данных - это основа хорошего пользовательского опыта и надёжного приложения.