{"id":24755163,"url":"https://github.com/gravity-ui/data-source","last_synced_at":"2025-10-11T01:31:25.107Z","repository":{"id":250668482,"uuid":"823742579","full_name":"gravity-ui/data-source","owner":"gravity-ui","description":"A wrapper around data fetching","archived":false,"fork":false,"pushed_at":"2025-01-22T16:01:34.000Z","size":456,"stargazers_count":20,"open_issues_count":2,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-01-22T17:19:15.209Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/gravity-ui.png","metadata":{"files":{"readme":"README-ru.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":"CODEOWNERS","security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-07-03T16:07:43.000Z","updated_at":"2025-01-22T16:01:37.000Z","dependencies_parsed_at":"2024-07-29T09:09:57.740Z","dependency_job_id":"d6e56cbd-6bd9-40be-99a6-09483bfa853a","html_url":"https://github.com/gravity-ui/data-source","commit_stats":null,"previous_names":["gravity-ui/data-source"],"tags_count":13,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gravity-ui%2Fdata-source","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gravity-ui%2Fdata-source/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gravity-ui%2Fdata-source/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gravity-ui%2Fdata-source/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/gravity-ui","download_url":"https://codeload.github.com/gravity-ui/data-source/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":236017790,"owners_count":19081980,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-01-28T12:36:40.663Z","updated_at":"2025-10-11T01:31:25.101Z","avatar_url":"https://github.com/gravity-ui.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Data Source \u0026middot; [![npm version](https://img.shields.io/npm/v/@gravity-ui/data-source?logo=npm\u0026label=version)](https://www.npmjs.com/package/@gravity-ui/data-source) [![ci](https://img.shields.io/github/actions/workflow/status/gravity-ui/data-source/ci.yml?branch=main\u0026label=ci\u0026logo=github)](https://github.com/gravity-ui/data-source/actions/workflows/ci.yml?query=branch:main)\n\n`Data Source` — это простой оберточный компонент для фетчинга данных. В рамках [чистой архитектуры](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) он выполняет роль порта, позволяя создавать обертки для различных сценариев работы с данными. `Data Source` в своей основе использует [`react-query`](https://tanstack.com/query/latest).\n\n## Установка\n\n```bash\nnpm install @gravity-ui/data-source @tanstack/react-query\n```\n\n`@tanstack/react-query` является peer-зависимостью.\n\n## Быстрый старт\n\n### 1. Настройка DataManager\n\nСначала создайте и предоставьте `DataManager` в вашем приложении:\n\n```tsx\nimport React from 'react';\nimport {ClientDataManager, DataManagerContext} from '@gravity-ui/data-source';\n\nconst dataManager = new ClientDataManager({\n  defaultOptions: {\n    queries: {\n      staleTime: 5 * 60 * 1000, // 5 минут\n      retry: 3,\n    },\n    // ... другие опции react-query\n  },\n});\n\nfunction App() {\n  return (\n    \u003cDataManagerContext.Provider value={dataManager}\u003e\n      \u003cYourApplication /\u003e\n    \u003c/DataManagerContext.Provider\u003e\n  );\n}\n```\n\n### 2. Определение типов ошибок и оберток\n\nОпределите тип ошибки и создайте свои конструкторы для источников данных на основе стандартных конструкторов:\n\n```ts\nimport {makePlainQueryDataSource as makePlainQueryDataSourceBase} from '@gravity-ui/data-source';\n\nexport interface ApiError {\n  code: number;\n  title: string;\n  description?: string;\n}\n\nexport const makePlainQueryDataSource = \u003cTParams, TRequest, TResponse, TData, TError = ApiError\u003e(\n  config: Omit\u003cPlainQueryDataSource\u003cTParams, TRequest, TResponse, TData, TError\u003e, 'type'\u003e,\n): PlainQueryDataSource\u003cTParams, TRequest, TResponse, TData, TError\u003e =\u003e {\n  return makePlainQueryDataSourceBase(config);\n};\n```\n\n### 3. Создание кастомного компонента DataLoader\n\nСоздайте компонент `DataLoader` на основе стандартного для определения отображения статуса загрузки и ошибок:\n\n```tsx\nimport {\n  DataLoader as DataLoaderBase,\n  DataLoaderProps as DataLoaderPropsBase,\n  ErrorViewProps,\n} from '@gravity-ui/data-source';\n\nexport interface DataLoaderProps\n  extends Omit\u003cDataLoaderPropsBase\u003cApiError\u003e, 'LoadingView' | 'ErrorView'\u003e {\n  LoadingView?: ComponentType;\n  ErrorView?: ComponentType\u003cErrorViewProps\u003cApiError\u003e\u003e;\n}\n\nexport const DataLoader: React.FC\u003cDataLoaderProps\u003e = ({\n  LoadingView = YourLoader, // Вы можете использовать свой компонент загрузки\n  ErrorView = YourError, // Вы можете использовать свой компонент ошибки\n  ...restProps\n}) =\u003e {\n  return \u003cDataLoaderBase LoadingView={LoadingView} ErrorView={ErrorView} {...restProps} /\u003e;\n};\n```\n\n### 4. Определение вашего первого источника данных\n\n```ts\nimport {skipContext} from '@gravity-ui/data-source';\n\n// Ваша API функция\nimport {fetchUser} from './api';\n\nexport const userDataSource = makePlainQueryDataSource({\n  // Ключи должны быть уникальными. Возможно, вам стоит создать помощник для создания имен источников данных\n  name: 'user',\n  // skipContext - это помощник для пропуска 2 первых параметров в функции (context и fetchContext)\n  fetch: skipContext(fetchUser),\n  // Опционально: генерация тегов для расширенной инвалидации кеша\n  tags: (params) =\u003e [`user:${params.userId}`, 'users'],\n});\n```\n\n### 5. Использование в компонентах\n\n```tsx\nimport {useQueryData} from '@gravity-ui/data-source';\n\nexport const UserProfile: React.FC\u003c{userId: number}\u003e = ({userId}) =\u003e {\n  const {data, status, error, refetch} = useQueryData(userDataSource, {userId});\n\n  return (\n    \u003cDataLoader status={status} error={error} errorAction={refetch}\u003e\n      {data \u0026\u0026 \u003cUserCard user={data} /\u003e}\n    \u003c/DataLoader\u003e\n  );\n};\n```\n\n## Основные концепции\n\n### Типы источников данных\n\nБиблиотека предоставляет два основных типа источников данных:\n\n#### Plain Query Data Source\n\nДля простых паттернов запрос/ответ:\n\n```ts\nconst userDataSource = makePlainQueryDataSource({\n  name: 'user',\n  fetch: skipContext(async (params: {userId: number}) =\u003e {\n    const response = await fetch(`/api/users/${params.userId}`);\n    return response.json();\n  }),\n});\n```\n\n#### Infinite Query Data Source\n\nДля пагинации и бесконечной прокрутки:\n\n```ts\nconst postsDataSource = makeInfiniteQueryDataSource({\n  name: 'posts',\n  fetch: skipContext(async (params: {page: number; limit: number}) =\u003e {\n    const response = await fetch(`/api/posts?page=${params.page}\u0026limit=${params.limit}`);\n    return response.json();\n  }),\n  next: (lastPage, allPages) =\u003e {\n    if (lastPage.hasNext) {\n      return {page: allPages.length + 1, limit: 20};\n    }\n    return undefined;\n  },\n});\n```\n\n### Управление статусами\n\nБиблиотека нормализует состояния запросов в три простых статуса:\n\n- `loading` - Фактическая загрузка данных. То же, что и `isLoading` в React Query\n- `success` - Данные доступны (могут быть пропущены с помощью idle)\n- `error` - Не удалось загрузить данные\n\n### Концепция idle\n\nБиблиотека предоставляет специальный символ `idle` для пропуска выполнения запросов:\n\n```ts\nimport {idle} from '@gravity-ui/data-source';\n\nconst UserProfile: React.FC\u003c{userId?: number}\u003e = ({userId}) =\u003e {\n  // Запрос не будет выполнен, если userId не определен\n  const {data, status} = useQueryData(userDataSource, userId ? {userId} : idle);\n\n  return (\n    \u003cDataLoader status={status} error={null}\u003e\n      {data \u0026\u0026 \u003cUserCard user={data} /\u003e}\n    \u003c/DataLoader\u003e\n  );\n};\n```\n\nКогда параметры равны `idle`:\n\n- Запрос не выполняется\n- Статус остается `success`\n- Данные остаются `undefined`\n- Компонент может безопасно рендериться без загрузки\n\n**Преимущества `idle`:**\n\n1. **Типобезопасность** - TypeScript правильно выводит типы для условных параметров\n2. **Производительность** - избегает ненужных запросов к серверу\n3. **Простота логики** - не нужно дополнительно управлять состоянием `enabled`\n4. **Консистентность** - унифицированный подход для всех условных запросов\n\nЭто особенно полезно для условных запросов, когда вы хотите загружать данные только при определенных условиях, сохраняя при этом типобезопасность.\n\n## Справочник API\n\n### Создание источников данных\n\n#### `makePlainQueryDataSource(config)`\n\nСоздает простой источник данных запросов для простых паттернов запрос/ответ.\n\n```ts\nconst dataSource = makePlainQueryDataSource({\n  name: 'unique-name',\n  fetch: skipContext(fetchFunction),\n  transformParams: (params) =\u003e transformedRequest,\n  transformResponse: (response) =\u003e transformedData,\n  tags: (params) =\u003e ['tag1', 'tag2'],\n  options: {\n    staleTime: 60000,\n    retry: 3,\n    // ... другие опции react-query\n  },\n});\n```\n\n**Параметры:**\n\n- `name` - Уникальный идентификатор для источника данных\n- `fetch` - Функция, которая выполняет фактическую загрузку данных\n- `transformParams` (опционально) - Преобразование входных параметров перед запросом\n- `transformResponse` (опционально) - Преобразование данных ответа\n- `tags` (опционально) - Генерация тегов кеша для инвалидации\n- `options` (опционально) - Опции React Query\n\n#### `makeInfiniteQueryDataSource(config)`\n\nСоздает бесконечный источник данных запросов для пагинации и паттернов бесконечной прокрутки.\n\n```ts\nconst infiniteDataSource = makeInfiniteQueryDataSource({\n  name: 'infinite-data',\n  fetch: skipContext(fetchFunction),\n  next: (lastPage, allPages) =\u003e nextPageParam || undefined,\n  prev: (firstPage, allPages) =\u003e prevPageParam || undefined,\n  // ... другие опции те же, что и для простого\n});\n```\n\n**Дополнительные параметры:**\n\n- `next` - Функция для определения параметров следующей страницы\n- `prev` (опционально) - Функция для определения параметров предыдущей страницы\n\n### React хуки\n\n#### `useQueryData(dataSource, params, options?)`\n\nОсновной хук для загрузки данных с источником данных.\n\n```ts\nconst {data, status, error, refetch, ...rest} = useQueryData(\n  userDataSource,\n  {userId: 123},\n  {\n    enabled: true,\n    refetchInterval: 30000,\n  },\n);\n```\n\n**Возвращает:**\n\n- `data` - Загруженные данные\n- `status` - Текущий статус ('loading' | 'success' | 'error')\n- `error` - Объект ошибки, если запрос не удался\n- `refetch` - Функция для ручной перезагрузки данных\n- Другие свойства React Query\n\n#### `useQueryResponses(responses)`\n\nОбъединяет несколько ответов запросов в одно состояние.\n\n```ts\nconst user = useQueryData(userDataSource, {userId});\nconst posts = useQueryData(postsDataSource, {userId});\n\nconst {status, error, refetch, refetchErrored} = useQueryResponses([user, posts]);\n```\n\n**Возвращает:**\n\n- `status` - Объединенный статус всех запросов\n- `error` - Первая встреченная ошибка\n- `refetch` - Функция для перезагрузки всех запросов\n- `refetchErrored` - Функция для перезагрузки только неудачных запросов\n\n#### `useRefetchAll(states)`\n\nСоздает callback для перезагрузки нескольких запросов.\n\n```ts\nconst refetchAll = useRefetchAll([user, posts, comments]);\n// refetchAll() запустит refetch для всех запросов\n```\n\n#### `useRefetchErrored(states)`\n\nСоздает callback для перезагрузки только неудачных запросов.\n\n```ts\nconst refetchErrored = useRefetchErrored([user, posts, comments]);\n// refetchErrored() перезагрузит только запросы с ошибками\n```\n\n#### `useDataManager()`\n\nВозвращает DataManager из контекста.\n\n```ts\nconst dataManager = useDataManager();\nawait dataManager.invalidateTag('users');\n```\n\n#### `useQueryContext()`\n\nВозвращает контекст запроса (для создания кастомных хуков данных на основе react-query).\n\n### React компоненты\n\n#### `\u003cDataLoader /\u003e`\n\nКомпонент для обработки состояний загрузки и ошибок.\n\n```tsx\n\u003cDataLoader\n  status={status}\n  error={error}\n  errorAction={refetch}\n  LoadingView={SpinnerComponent}\n  ErrorView={ErrorComponent}\n  loadingViewProps={{size: 'large'}}\n  errorViewProps={{showDetails: true}}\n\u003e\n  {data \u0026\u0026 \u003cYourContent data={data} /\u003e}\n\u003c/DataLoader\u003e\n```\n\n**Props:**\n\n- `status` - Текущий статус загрузки\n- `error` - Объект ошибки\n- `errorAction` - Функция или конфигурация действия для повтора при ошибке\n- `LoadingView` - Компонент для отображения во время загрузки\n- `ErrorView` - Компонент для отображения при ошибке\n- `loadingViewProps` - Props, передаваемые в LoadingView\n- `errorViewProps` - Props, передаваемые в ErrorView\n\n#### `\u003cDataInfiniteLoader /\u003e`\n\nСпециализированный компонент для бесконечных запросов.\n\n```tsx\n\u003cDataInfiniteLoader\n  status={status}\n  error={error}\n  hasNextPage={hasNextPage}\n  fetchNextPage={fetchNextPage}\n  isFetchingNextPage={isFetchingNextPage}\n  LoadingView={SpinnerComponent}\n  ErrorView={ErrorComponent}\n  MoreView={LoadMoreButton}\n\u003e\n  {data.map((item) =\u003e (\n    \u003cItem key={item.id} data={item} /\u003e\n  ))}\n\u003c/DataInfiniteLoader\u003e\n```\n\n**Дополнительные Props:**\n\n- `hasNextPage` - Доступны ли еще страницы\n- `fetchNextPage` - Функция для загрузки следующей страницы\n- `isFetchingNextPage` - Загружается ли следующая страница\n- `MoreView` - Компонент для кнопки \"загрузить еще\"\n\n#### `withDataManager(Component)`\n\nHOC, который инжектирует DataManager как prop.\n\n```tsx\nconst MyComponent = withDataManager\u003cProps\u003e(({dataManager, ...props}) =\u003e {\n  // Компонент имеет доступ к dataManager\n  return \u003cdiv\u003e...\u003c/div\u003e;\n});\n```\n\n### Управление данными\n\n#### `ClientDataManager`\n\nОсновной класс для управления данными.\n\n```ts\nconst dataManager = new ClientDataManager({\n  defaultOptions: {\n    queries: {\n      staleTime: 300000, // 5 минут\n      retry: 3,\n      refetchOnWindowFocus: false,\n    },\n  },\n});\n```\n\n**Методы:**\n\n##### `invalidateTag(tag, options?)`\n\nИнвалидация всех запросов с определенным тегом.\n\n```ts\nawait dataManager.invalidateTag('users');\nawait dataManager.invalidateTag('posts', {\n  repeat: {count: 3, interval: 1000}, // Повтор инвалидации\n});\n```\n\n##### `invalidateTags(tags, options?)`\n\nИнвалидация запросов, которые имеют все указанные теги.\n\n```ts\nawait dataManager.invalidateTags(['user', 'profile']);\n```\n\n##### `invalidateSource(dataSource, options?)`\n\nИнвалидация всех запросов для источника данных.\n\n```ts\nawait dataManager.invalidateSource(userDataSource);\n```\n\n##### `invalidateParams(dataSource, params, options?)`\n\nИнвалидация конкретного запроса с точными параметрами.\n\n```ts\nawait dataManager.invalidateParams(userDataSource, {userId: 123});\n```\n\n##### `resetSource(dataSource)`\n\nСброс (очистка) всех кешированных данных для источника данных.\n\n```ts\nawait dataManager.resetSource(userDataSource);\n```\n\n##### `resetParams(dataSource, params)`\n\nСброс кешированных данных для конкретных параметров.\n\n```ts\nawait dataManager.resetParams(userDataSource, {userId: 123});\n```\n\n##### `invalidateSourceTags(dataSource, params, options?)`\n\nИнвалидация запросов на основе тегов, сгенерированных источником данных.\n\n```ts\nawait dataManager.invalidateSourceTags(userDataSource, {userId: 123});\n```\n\n### Утилиты\n\n#### `skipContext(fetchFunction)`\n\nУтилита для адаптации существующих функций загрузки к интерфейсу источника данных.\n\n```ts\n// Существующая функция\nasync function fetchUser(params: {userId: number}) {\n  // ...\n}\n\n// Адаптированная для источника данных\nconst dataSource = makePlainQueryDataSource({\n  name: 'user',\n  fetch: skipContext(fetchUser), // Пропускает параметры context и fetchContext\n});\n```\n\n#### `withCatch(fetchFunction, errorHandler)`\n\nДобавляет стандартизированную обработку ошибок к функциям загрузки.\n\n```ts\nconst safeFetch = withCatch(fetchUser, (error) =\u003e ({error: true, message: error.message}));\n```\n\n#### `withCancellation(fetchFunction)`\n\nДобавляет поддержку отмены к функциям загрузки.\n\n```ts\nconst cancellableFetch = withCancellation(fetchFunction);\n// Автоматически обрабатывает AbortSignal от React Query\n```\n\n#### `getProgressiveRefetch(options)`\n\nСоздает функцию прогрессивного интервала перезагрузки.\n\n```ts\nconst progressiveRefetch = getProgressiveRefetch({\n  minInterval: 1000, // Начать с 1 секунды\n  maxInterval: 30000, // Максимум 30 секунд\n  multiplier: 2, // Удваивать каждый раз\n});\n\nconst dataSource = makePlainQueryDataSource({\n  name: 'data',\n  fetch: skipContext(fetchData),\n  options: {\n    refetchInterval: progressiveRefetch,\n  },\n});\n```\n\n#### `normalizeStatus(status, fetchStatus)`\n\nПреобразует статусы React Query в статус DataLoader.\n\n```ts\nconst status = normalizeStatus('pending', 'fetching'); // 'loading'\n```\n\n#### Утилиты статусов и ошибок\n\n```ts\n// Получить объединенный статус из нескольких состояний\nconst status = getStatus([user, posts, comments]);\n\n// Получить первую ошибку из нескольких состояний\nconst error = getError([user, posts, comments]);\n\n// Объединить несколько статусов\nconst combinedStatus = mergeStatuses(['loading', 'success', 'error']); // 'error'\n\n// Проверить, есть ли у ключа запроса тег\nconst hasUserTag = hasTag(queryKey, 'users');\n```\n\n#### Константы\n\n```ts\nimport {idle} from '@gravity-ui/data-source';\n\n// Специальный символ для пропуска выполнения запросов\nconst params = shouldFetch ? {userId: 123} : idle;\n\n// Типобезопасная альтернатива enabled: false\n// Вместо:\nconst {data} = useQueryData(userDataSource, {userId: userId || ''}, {enabled: Boolean(userId)});\n\n// Используйте:\nconst {data} = useQueryData(userDataSource, userId ? {userId} : idle);\n// TypeScript корректно выводит типы для обеих веток\n```\n\n#### Утилиты композиции ключей\n\n```ts\n// Составить ключ кеша для источника данных\nconst key = composeKey(userDataSource, {userId: 123});\n\n// Составить полный ключ, включая теги\nconst fullKey = composeFullKey(userDataSource, {userId: 123});\n```\n\n#### Композиция опций запросов\n\n```ts\n// Составить опции React Query для простых запросов\nconst plainOptions = composePlainQueryOptions(context, dataSource, params, options);\n\n// Составить опции React Query для бесконечных запросов\nconst infiniteOptions = composeInfiniteQueryOptions(context, dataSource, params, options);\n```\n\n**Примечание:** Эти функции в основном для внутреннего использования при создании кастомных реализаций источников данных.\n\n## Продвинутые паттерны\n\n### Условные запросы с idle\n\nИспользуйте `idle` для создания условных запросов:\n\n```ts\nimport {idle} from '@gravity-ui/data-source';\n\nconst ConditionalDataComponent: React.FC\u003c{\n  userId?: number;\n  shouldLoadPosts: boolean;\n}\u003e = ({userId, shouldLoadPosts}) =\u003e {\n  // Загружать пользователя только если userId определен\n  const user = useQueryData(\n    userDataSource,\n    userId ? {userId} : idle\n  );\n\n  // Загружать посты только если пользователь загружен и включен флаг\n  const posts = useQueryData(\n    userPostsDataSource,\n    user.data \u0026\u0026 shouldLoadPosts ? {userId: user.data.id} : idle\n  );\n\n  const combined = useQueryResponses([user, posts]);\n\n  return (\n    \u003cDataLoader status={combined.status} error={combined.error}\u003e\n      \u003cdiv\u003e\n        {user.data \u0026\u0026 \u003cUserInfo user={user.data} /\u003e}\n        {posts.data \u0026\u0026 \u003cUserPosts posts={posts.data} /\u003e}\n      \u003c/div\u003e\n    \u003c/DataLoader\u003e\n  );\n};\n```\n\n### Преобразование данных\n\nПреобразование параметров запроса и данных ответа:\n\n```ts\nconst apiDataSource = makePlainQueryDataSource({\n  name: 'api-data',\n  transformParams: (params: {id: number}) =\u003e ({\n    userId: params.id,\n    apiVersion: 'v2',\n    format: 'json',\n  }),\n  transformResponse: (response: ApiResponse) =\u003e ({\n    user: response.data.user,\n    metadata: response.meta,\n  }),\n  fetch: skipContext(apiFetch),\n});\n```\n\n### Инвалидация кеша на основе тегов\n\nИспользуйте теги для сложного управления кешем:\n\n```ts\nconst userDataSource = makePlainQueryDataSource({\n  name: 'user',\n  tags: (params) =\u003e [`user:${params.userId}`, 'users', 'profiles'],\n  fetch: skipContext(fetchUser),\n});\n\nconst userPostsDataSource = makePlainQueryDataSource({\n  name: 'user-posts',\n  tags: (params) =\u003e [`user:${params.userId}`, 'posts'],\n  fetch: skipContext(fetchUserPosts),\n});\n\n// Инвалидировать все данные для конкретного пользователя\nawait dataManager.invalidateTag('user:123');\n\n// Инвалидировать все данные, связанные с пользователями\nawait dataManager.invalidateTag('users');\n```\n\n### Обработка ошибок с типами\n\nСоздайте типобезопасную обработку ошибок:\n\n```ts\ninterface ApiError {\n  code: number;\n  message: string;\n  details?: Record\u003cstring, unknown\u003e;\n}\n\nconst ErrorView: React.FC\u003cErrorViewProps\u003cApiError\u003e\u003e = ({error, action}) =\u003e (\n  \u003cdiv className=\"error\"\u003e\n    \u003ch3\u003eОшибка {error?.code}\u003c/h3\u003e\n    \u003cp\u003e{error?.message}\u003c/p\u003e\n    {action \u0026\u0026 (\n      \u003cbutton onClick={action.handler}\u003e\n        {action.children || 'Повторить'}\n      \u003c/button\u003e\n    )}\n  \u003c/div\u003e\n);\n```\n\n### Бесконечные запросы со сложной пагинацией\n\nОбработка сложных сценариев пагинации:\n\n```ts\ninterface PaginationParams {\n  cursor?: string;\n  limit?: number;\n  filters?: Record\u003cstring, unknown\u003e;\n}\n\ninterface PaginatedResponse\u003cT\u003e {\n  data: T[];\n  nextCursor?: string;\n  hasMore: boolean;\n}\n\nconst infiniteDataSource = makeInfiniteQueryDataSource({\n  name: 'paginated-data',\n  fetch: skipContext(async (params: PaginationParams) =\u003e {\n    const response = await fetch(`/api/data?${new URLSearchParams(params)}`);\n    return response.json() as PaginatedResponse\u003cDataItem\u003e;\n  }),\n  next: (lastPage) =\u003e {\n    if (lastPage.hasMore \u0026\u0026 lastPage.nextCursor) {\n      return {cursor: lastPage.nextCursor, limit: 20};\n    }\n    return undefined;\n  },\n});\n```\n\n### Объединение нескольких источников данных\n\nОбъединение данных из нескольких источников:\n\n```ts\nconst UserProfile: React.FC\u003c{userId: number}\u003e = ({userId}) =\u003e {\n  const user = useQueryData(userDataSource, {userId});\n  const posts = useQueryData(userPostsDataSource, {userId});\n  const followers = useQueryData(userFollowersDataSource, {userId});\n\n  const combined = useQueryResponses([user, posts, followers]);\n\n  return (\n    \u003cDataLoader\n      status={combined.status}\n      error={combined.error}\n      errorAction={combined.refetchErrored} // Повторить только неудачные запросы\n      LoadingView={ProfileSkeleton}\n      ErrorView={ProfileError}\n    \u003e\n      {user \u0026\u0026 posts \u0026\u0026 followers \u0026\u0026 (\n        \u003cdiv\u003e\n          \u003cUserInfo user={user.data} /\u003e\n          \u003cUserPosts posts={posts.data} /\u003e\n          \u003cUserFollowers followers={followers.data} /\u003e\n        \u003c/div\u003e\n      )}\n    \u003c/DataLoader\u003e\n  );\n};\n```\n\n## Поддержка TypeScript\n\nБиблиотека построена с TypeScript-first подходом и обеспечивает полный вывод типов:\n\n```ts\n// Типы автоматически выводятся\nconst userDataSource = makePlainQueryDataSource({\n  name: 'user',\n  fetch: skipContext(async (params: {userId: number}): Promise\u003cUser\u003e =\u003e {\n    // Тип возврата выводится как User\n  }),\n});\n\n// Тип возврата хука автоматически типизирован\nconst {data} = useQueryData(userDataSource, {userId: 123});\n// data типизирован как User | undefined\n```\n\n### Кастомные типы ошибок\n\nОпределение и использование кастомных типов ошибок:\n\n```ts\ninterface ValidationError {\n  field: string;\n  message: string;\n}\n\ninterface ApiError {\n  type: 'network' | 'validation' | 'server';\n  message: string;\n  validation?: ValidationError[];\n}\n\nconst typedDataSource = makePlainQueryDataSource\u003c\n  {id: number}, // Тип параметров\n  {id: number}, // Тип запроса\n  ApiResponse, // Тип ответа\n  User, // Тип данных\n  ApiError // Тип ошибки\n\u003e({\n  name: 'typed-user',\n  fetch: skipContext(fetchUser),\n});\n```\n\n## Содействие проекту\n\nПожалуйста, прочтите [CONTRIBUTING.md](CONTRIBUTING.md) для получения информации о нашем кодексе поведения и процессе отправки pull request'ов.\n\n## Лицензия\n\nMIT License. См. файл [LICENSE](LICENSE) для деталей.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgravity-ui%2Fdata-source","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgravity-ui%2Fdata-source","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgravity-ui%2Fdata-source/lists"}