https://github.com/gravity-ui/data-source
A wrapper around data fetching
https://github.com/gravity-ui/data-source
Last synced: 8 months ago
JSON representation
A wrapper around data fetching
- Host: GitHub
- URL: https://github.com/gravity-ui/data-source
- Owner: gravity-ui
- License: mit
- Created: 2024-07-03T16:07:43.000Z (almost 2 years ago)
- Default Branch: main
- Last Pushed: 2025-01-22T16:01:34.000Z (over 1 year ago)
- Last Synced: 2025-01-22T17:19:15.209Z (over 1 year ago)
- Language: TypeScript
- Homepage:
- Size: 445 KB
- Stars: 20
- Watchers: 1
- Forks: 0
- Open Issues: 2
-
Metadata Files:
- Readme: README-ru.md
- Changelog: CHANGELOG.md
- Contributing: CONTRIBUTING.md
- License: LICENSE
- Codeowners: CODEOWNERS
Awesome Lists containing this project
README
# Data Source · [](https://www.npmjs.com/package/@gravity-ui/data-source) [](https://github.com/gravity-ui/data-source/actions/workflows/ci.yml?query=branch:main)
`Data Source` — это простой оберточный компонент для фетчинга данных. В рамках [чистой архитектуры](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) он выполняет роль порта, позволяя создавать обертки для различных сценариев работы с данными. `Data Source` в своей основе использует [`react-query`](https://tanstack.com/query/latest).
## Установка
```bash
npm install @gravity-ui/data-source @tanstack/react-query
```
`@tanstack/react-query` является peer-зависимостью.
## Быстрый старт
### 1. Настройка DataManager
Сначала создайте и предоставьте `DataManager` в вашем приложении:
```tsx
import React from 'react';
import {ClientDataManager, DataManagerContext} from '@gravity-ui/data-source';
const dataManager = new ClientDataManager({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 минут
retry: 3,
},
// ... другие опции react-query
},
});
function App() {
return (
);
}
```
### 2. Определение типов ошибок и оберток
Определите тип ошибки и создайте свои конструкторы для источников данных на основе стандартных конструкторов:
```ts
import {makePlainQueryDataSource as makePlainQueryDataSourceBase} from '@gravity-ui/data-source';
export interface ApiError {
code: number;
title: string;
description?: string;
}
export const makePlainQueryDataSource = (
config: Omit, 'type'>,
): PlainQueryDataSource => {
return makePlainQueryDataSourceBase(config);
};
```
### 3. Создание кастомного компонента DataLoader
Создайте компонент `DataLoader` на основе стандартного для определения отображения статуса загрузки и ошибок:
```tsx
import {
DataLoader as DataLoaderBase,
DataLoaderProps as DataLoaderPropsBase,
ErrorViewProps,
} from '@gravity-ui/data-source';
export interface DataLoaderProps
extends Omit, 'LoadingView' | 'ErrorView'> {
LoadingView?: ComponentType;
ErrorView?: ComponentType>;
}
export const DataLoader: React.FC = ({
LoadingView = YourLoader, // Вы можете использовать свой компонент загрузки
ErrorView = YourError, // Вы можете использовать свой компонент ошибки
...restProps
}) => {
return ;
};
```
### 4. Определение вашего первого источника данных
```ts
import {skipContext} from '@gravity-ui/data-source';
// Ваша API функция
import {fetchUser} from './api';
export const userDataSource = makePlainQueryDataSource({
// Ключи должны быть уникальными. Возможно, вам стоит создать помощник для создания имен источников данных
name: 'user',
// skipContext - это помощник для пропуска 2 первых параметров в функции (context и fetchContext)
fetch: skipContext(fetchUser),
// Опционально: генерация тегов для расширенной инвалидации кеша
tags: (params) => [`user:${params.userId}`, 'users'],
});
```
### 5. Использование в компонентах
```tsx
import {useQueryData} from '@gravity-ui/data-source';
export const UserProfile: React.FC<{userId: number}> = ({userId}) => {
const {data, status, error, refetch} = useQueryData(userDataSource, {userId});
return (
{data && }
);
};
```
## Основные концепции
### Типы источников данных
Библиотека предоставляет два основных типа источников данных:
#### Plain Query Data Source
Для простых паттернов запрос/ответ:
```ts
const userDataSource = makePlainQueryDataSource({
name: 'user',
fetch: skipContext(async (params: {userId: number}) => {
const response = await fetch(`/api/users/${params.userId}`);
return response.json();
}),
});
```
#### Infinite Query Data Source
Для пагинации и бесконечной прокрутки:
```ts
const postsDataSource = makeInfiniteQueryDataSource({
name: 'posts',
fetch: skipContext(async (params: {page: number; limit: number}) => {
const response = await fetch(`/api/posts?page=${params.page}&limit=${params.limit}`);
return response.json();
}),
next: (lastPage, allPages) => {
if (lastPage.hasNext) {
return {page: allPages.length + 1, limit: 20};
}
return undefined;
},
});
```
### Управление статусами
Библиотека нормализует состояния запросов в три простых статуса:
- `loading` - Фактическая загрузка данных. То же, что и `isLoading` в React Query
- `success` - Данные доступны (могут быть пропущены с помощью idle)
- `error` - Не удалось загрузить данные
### Концепция idle
Библиотека предоставляет специальный символ `idle` для пропуска выполнения запросов:
```ts
import {idle} from '@gravity-ui/data-source';
const UserProfile: React.FC<{userId?: number}> = ({userId}) => {
// Запрос не будет выполнен, если userId не определен
const {data, status} = useQueryData(userDataSource, userId ? {userId} : idle);
return (
{data && }
);
};
```
Когда параметры равны `idle`:
- Запрос не выполняется
- Статус остается `success`
- Данные остаются `undefined`
- Компонент может безопасно рендериться без загрузки
**Преимущества `idle`:**
1. **Типобезопасность** - TypeScript правильно выводит типы для условных параметров
2. **Производительность** - избегает ненужных запросов к серверу
3. **Простота логики** - не нужно дополнительно управлять состоянием `enabled`
4. **Консистентность** - унифицированный подход для всех условных запросов
Это особенно полезно для условных запросов, когда вы хотите загружать данные только при определенных условиях, сохраняя при этом типобезопасность.
## Справочник API
### Создание источников данных
#### `makePlainQueryDataSource(config)`
Создает простой источник данных запросов для простых паттернов запрос/ответ.
```ts
const dataSource = makePlainQueryDataSource({
name: 'unique-name',
fetch: skipContext(fetchFunction),
transformParams: (params) => transformedRequest,
transformResponse: (response) => transformedData,
tags: (params) => ['tag1', 'tag2'],
options: {
staleTime: 60000,
retry: 3,
// ... другие опции react-query
},
});
```
**Параметры:**
- `name` - Уникальный идентификатор для источника данных
- `fetch` - Функция, которая выполняет фактическую загрузку данных
- `transformParams` (опционально) - Преобразование входных параметров перед запросом
- `transformResponse` (опционально) - Преобразование данных ответа
- `tags` (опционально) - Генерация тегов кеша для инвалидации
- `options` (опционально) - Опции React Query
#### `makeInfiniteQueryDataSource(config)`
Создает бесконечный источник данных запросов для пагинации и паттернов бесконечной прокрутки.
```ts
const infiniteDataSource = makeInfiniteQueryDataSource({
name: 'infinite-data',
fetch: skipContext(fetchFunction),
next: (lastPage, allPages) => nextPageParam || undefined,
prev: (firstPage, allPages) => prevPageParam || undefined,
// ... другие опции те же, что и для простого
});
```
**Дополнительные параметры:**
- `next` - Функция для определения параметров следующей страницы
- `prev` (опционально) - Функция для определения параметров предыдущей страницы
### React хуки
#### `useQueryData(dataSource, params, options?)`
Основной хук для загрузки данных с источником данных.
```ts
const {data, status, error, refetch, ...rest} = useQueryData(
userDataSource,
{userId: 123},
{
enabled: true,
refetchInterval: 30000,
},
);
```
**Возвращает:**
- `data` - Загруженные данные
- `status` - Текущий статус ('loading' | 'success' | 'error')
- `error` - Объект ошибки, если запрос не удался
- `refetch` - Функция для ручной перезагрузки данных
- Другие свойства React Query
#### `useQueryResponses(responses)`
Объединяет несколько ответов запросов в одно состояние.
```ts
const user = useQueryData(userDataSource, {userId});
const posts = useQueryData(postsDataSource, {userId});
const {status, error, refetch, refetchErrored} = useQueryResponses([user, posts]);
```
**Возвращает:**
- `status` - Объединенный статус всех запросов
- `error` - Первая встреченная ошибка
- `refetch` - Функция для перезагрузки всех запросов
- `refetchErrored` - Функция для перезагрузки только неудачных запросов
#### `useRefetchAll(states)`
Создает callback для перезагрузки нескольких запросов.
```ts
const refetchAll = useRefetchAll([user, posts, comments]);
// refetchAll() запустит refetch для всех запросов
```
#### `useRefetchErrored(states)`
Создает callback для перезагрузки только неудачных запросов.
```ts
const refetchErrored = useRefetchErrored([user, posts, comments]);
// refetchErrored() перезагрузит только запросы с ошибками
```
#### `useDataManager()`
Возвращает DataManager из контекста.
```ts
const dataManager = useDataManager();
await dataManager.invalidateTag('users');
```
#### `useQueryContext()`
Возвращает контекст запроса (для создания кастомных хуков данных на основе react-query).
### React компоненты
#### ``
Компонент для обработки состояний загрузки и ошибок.
```tsx
{data && }
```
**Props:**
- `status` - Текущий статус загрузки
- `error` - Объект ошибки
- `errorAction` - Функция или конфигурация действия для повтора при ошибке
- `LoadingView` - Компонент для отображения во время загрузки
- `ErrorView` - Компонент для отображения при ошибке
- `loadingViewProps` - Props, передаваемые в LoadingView
- `errorViewProps` - Props, передаваемые в ErrorView
#### ``
Специализированный компонент для бесконечных запросов.
```tsx
{data.map((item) => (
))}
```
**Дополнительные Props:**
- `hasNextPage` - Доступны ли еще страницы
- `fetchNextPage` - Функция для загрузки следующей страницы
- `isFetchingNextPage` - Загружается ли следующая страница
- `MoreView` - Компонент для кнопки "загрузить еще"
#### `withDataManager(Component)`
HOC, который инжектирует DataManager как prop.
```tsx
const MyComponent = withDataManager(({dataManager, ...props}) => {
// Компонент имеет доступ к dataManager
return
...;
});
```
### Управление данными
#### `ClientDataManager`
Основной класс для управления данными.
```ts
const dataManager = new ClientDataManager({
defaultOptions: {
queries: {
staleTime: 300000, // 5 минут
retry: 3,
refetchOnWindowFocus: false,
},
},
});
```
**Методы:**
##### `invalidateTag(tag, options?)`
Инвалидация всех запросов с определенным тегом.
```ts
await dataManager.invalidateTag('users');
await dataManager.invalidateTag('posts', {
repeat: {count: 3, interval: 1000}, // Повтор инвалидации
});
```
##### `invalidateTags(tags, options?)`
Инвалидация запросов, которые имеют все указанные теги.
```ts
await dataManager.invalidateTags(['user', 'profile']);
```
##### `invalidateSource(dataSource, options?)`
Инвалидация всех запросов для источника данных.
```ts
await dataManager.invalidateSource(userDataSource);
```
##### `invalidateParams(dataSource, params, options?)`
Инвалидация конкретного запроса с точными параметрами.
```ts
await dataManager.invalidateParams(userDataSource, {userId: 123});
```
##### `resetSource(dataSource)`
Сброс (очистка) всех кешированных данных для источника данных.
```ts
await dataManager.resetSource(userDataSource);
```
##### `resetParams(dataSource, params)`
Сброс кешированных данных для конкретных параметров.
```ts
await dataManager.resetParams(userDataSource, {userId: 123});
```
##### `invalidateSourceTags(dataSource, params, options?)`
Инвалидация запросов на основе тегов, сгенерированных источником данных.
```ts
await dataManager.invalidateSourceTags(userDataSource, {userId: 123});
```
### Утилиты
#### `skipContext(fetchFunction)`
Утилита для адаптации существующих функций загрузки к интерфейсу источника данных.
```ts
// Существующая функция
async function fetchUser(params: {userId: number}) {
// ...
}
// Адаптированная для источника данных
const dataSource = makePlainQueryDataSource({
name: 'user',
fetch: skipContext(fetchUser), // Пропускает параметры context и fetchContext
});
```
#### `withCatch(fetchFunction, errorHandler)`
Добавляет стандартизированную обработку ошибок к функциям загрузки.
```ts
const safeFetch = withCatch(fetchUser, (error) => ({error: true, message: error.message}));
```
#### `withCancellation(fetchFunction)`
Добавляет поддержку отмены к функциям загрузки.
```ts
const cancellableFetch = withCancellation(fetchFunction);
// Автоматически обрабатывает AbortSignal от React Query
```
#### `getProgressiveRefetch(options)`
Создает функцию прогрессивного интервала перезагрузки.
```ts
const progressiveRefetch = getProgressiveRefetch({
minInterval: 1000, // Начать с 1 секунды
maxInterval: 30000, // Максимум 30 секунд
multiplier: 2, // Удваивать каждый раз
});
const dataSource = makePlainQueryDataSource({
name: 'data',
fetch: skipContext(fetchData),
options: {
refetchInterval: progressiveRefetch,
},
});
```
#### `normalizeStatus(status, fetchStatus)`
Преобразует статусы React Query в статус DataLoader.
```ts
const status = normalizeStatus('pending', 'fetching'); // 'loading'
```
#### Утилиты статусов и ошибок
```ts
// Получить объединенный статус из нескольких состояний
const status = getStatus([user, posts, comments]);
// Получить первую ошибку из нескольких состояний
const error = getError([user, posts, comments]);
// Объединить несколько статусов
const combinedStatus = mergeStatuses(['loading', 'success', 'error']); // 'error'
// Проверить, есть ли у ключа запроса тег
const hasUserTag = hasTag(queryKey, 'users');
```
#### Константы
```ts
import {idle} from '@gravity-ui/data-source';
// Специальный символ для пропуска выполнения запросов
const params = shouldFetch ? {userId: 123} : idle;
// Типобезопасная альтернатива enabled: false
// Вместо:
const {data} = useQueryData(userDataSource, {userId: userId || ''}, {enabled: Boolean(userId)});
// Используйте:
const {data} = useQueryData(userDataSource, userId ? {userId} : idle);
// TypeScript корректно выводит типы для обеих веток
```
#### Утилиты композиции ключей
```ts
// Составить ключ кеша для источника данных
const key = composeKey(userDataSource, {userId: 123});
// Составить полный ключ, включая теги
const fullKey = composeFullKey(userDataSource, {userId: 123});
```
#### Композиция опций запросов
```ts
// Составить опции React Query для простых запросов
const plainOptions = composePlainQueryOptions(context, dataSource, params, options);
// Составить опции React Query для бесконечных запросов
const infiniteOptions = composeInfiniteQueryOptions(context, dataSource, params, options);
```
**Примечание:** Эти функции в основном для внутреннего использования при создании кастомных реализаций источников данных.
## Продвинутые паттерны
### Условные запросы с idle
Используйте `idle` для создания условных запросов:
```ts
import {idle} from '@gravity-ui/data-source';
const ConditionalDataComponent: React.FC<{
userId?: number;
shouldLoadPosts: boolean;
}> = ({userId, shouldLoadPosts}) => {
// Загружать пользователя только если userId определен
const user = useQueryData(
userDataSource,
userId ? {userId} : idle
);
// Загружать посты только если пользователь загружен и включен флаг
const posts = useQueryData(
userPostsDataSource,
user.data && shouldLoadPosts ? {userId: user.data.id} : idle
);
const combined = useQueryResponses([user, posts]);
return (
{user.data && }
{posts.data && }
);
};
```
### Преобразование данных
Преобразование параметров запроса и данных ответа:
```ts
const apiDataSource = makePlainQueryDataSource({
name: 'api-data',
transformParams: (params: {id: number}) => ({
userId: params.id,
apiVersion: 'v2',
format: 'json',
}),
transformResponse: (response: ApiResponse) => ({
user: response.data.user,
metadata: response.meta,
}),
fetch: skipContext(apiFetch),
});
```
### Инвалидация кеша на основе тегов
Используйте теги для сложного управления кешем:
```ts
const userDataSource = makePlainQueryDataSource({
name: 'user',
tags: (params) => [`user:${params.userId}`, 'users', 'profiles'],
fetch: skipContext(fetchUser),
});
const userPostsDataSource = makePlainQueryDataSource({
name: 'user-posts',
tags: (params) => [`user:${params.userId}`, 'posts'],
fetch: skipContext(fetchUserPosts),
});
// Инвалидировать все данные для конкретного пользователя
await dataManager.invalidateTag('user:123');
// Инвалидировать все данные, связанные с пользователями
await dataManager.invalidateTag('users');
```
### Обработка ошибок с типами
Создайте типобезопасную обработку ошибок:
```ts
interface ApiError {
code: number;
message: string;
details?: Record;
}
const ErrorView: React.FC> = ({error, action}) => (
Ошибка {error?.code}
{error?.message}
{action && (
{action.children || 'Повторить'}
)}
);
```
### Бесконечные запросы со сложной пагинацией
Обработка сложных сценариев пагинации:
```ts
interface PaginationParams {
cursor?: string;
limit?: number;
filters?: Record;
}
interface PaginatedResponse {
data: T[];
nextCursor?: string;
hasMore: boolean;
}
const infiniteDataSource = makeInfiniteQueryDataSource({
name: 'paginated-data',
fetch: skipContext(async (params: PaginationParams) => {
const response = await fetch(`/api/data?${new URLSearchParams(params)}`);
return response.json() as PaginatedResponse;
}),
next: (lastPage) => {
if (lastPage.hasMore && lastPage.nextCursor) {
return {cursor: lastPage.nextCursor, limit: 20};
}
return undefined;
},
});
```
### Объединение нескольких источников данных
Объединение данных из нескольких источников:
```ts
const UserProfile: React.FC<{userId: number}> = ({userId}) => {
const user = useQueryData(userDataSource, {userId});
const posts = useQueryData(userPostsDataSource, {userId});
const followers = useQueryData(userFollowersDataSource, {userId});
const combined = useQueryResponses([user, posts, followers]);
return (
{user && posts && followers && (
)}
);
};
```
## Поддержка TypeScript
Библиотека построена с TypeScript-first подходом и обеспечивает полный вывод типов:
```ts
// Типы автоматически выводятся
const userDataSource = makePlainQueryDataSource({
name: 'user',
fetch: skipContext(async (params: {userId: number}): Promise => {
// Тип возврата выводится как User
}),
});
// Тип возврата хука автоматически типизирован
const {data} = useQueryData(userDataSource, {userId: 123});
// data типизирован как User | undefined
```
### Кастомные типы ошибок
Определение и использование кастомных типов ошибок:
```ts
interface ValidationError {
field: string;
message: string;
}
interface ApiError {
type: 'network' | 'validation' | 'server';
message: string;
validation?: ValidationError[];
}
const typedDataSource = makePlainQueryDataSource<
{id: number}, // Тип параметров
{id: number}, // Тип запроса
ApiResponse, // Тип ответа
User, // Тип данных
ApiError // Тип ошибки
>({
name: 'typed-user',
fetch: skipContext(fetchUser),
});
```
## Содействие проекту
Пожалуйста, прочтите [CONTRIBUTING.md](CONTRIBUTING.md) для получения информации о нашем кодексе поведения и процессе отправки pull request'ов.
## Лицензия
MIT License. См. файл [LICENSE](LICENSE) для деталей.