Data Source
Data Source ·

Data Source ist ein einfacher Wrapper für das Abrufen von Daten. Es ist eine Art "Port" in der Clean Architecture. Es ermöglicht Ihnen, Wrapper für Dinge rund um das Abrufen von Daten zu erstellen, je nach Ihren Anwendungsfällen. Data Source verwendet react-query im Hintergrund.
Installation
npm install @gravity-ui/data-source @tanstack/react-query
@tanstack/react-query ist eine Peer-Abhängigkeit.
Erste Schritte
1. DataManager einrichten
Erstellen und stellen Sie zuerst einen DataManager in Ihrer Anwendung bereit:
import React from 'react';
import {ClientDataManager, DataSourceProvider} from '@gravity-ui/data-source';
const dataManager = new ClientDataManager({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 Minuten
retry: 3,
},
// ... weitere react-query Optionen
},
});
function App() {
return (
<DataSourceProvider dataManager={dataManager}>
<YourApplication />
</DataSourceProvider>
);
}
2. Fehlertypen und Wrapper definieren
Definieren Sie einen Fehlertyp und erstellen Sie Ihre Konstruktoren für Datenquellen basierend auf den Standardkonstruktoren:
import {makePlainQueryDataSource as makePlainQueryDataSourceBase} from '@gravity-ui/data-source';
export interface ApiError {
code: number;
title: string;
description?: string;
}
export const makePlainQueryDataSource = <TParams, TRequest, TResponse, TData, TError = ApiError>(
config: Omit<PlainQueryDataSource<TParams, TRequest, TResponse, TData, TError>, 'type'>,
): PlainQueryDataSource<TParams, TRequest, TResponse, TData, TError> => {
return makePlainQueryDataSourceBase(config);
};
3. Benutzerdefinierten DataLoader-Komponenten erstellen
Schreiben Sie eine DataLoader-Komponente, die auf der Standardkomponente basiert, um Ihre Anzeige des Ladezustands und von Fehlern zu definieren:
import {
DataLoader as DataLoaderBase,
DataLoaderProps as DataLoaderPropsBase,
ErrorViewProps,
} from '@gravity-ui/data-source';
export interface DataLoaderProps
extends Omit<DataLoaderPropsBase<ApiError>, 'LoadingView' | 'ErrorView'> {
LoadingView?: ComponentType;
ErrorView?: ComponentType<ErrorViewProps<ApiError>>;
}
export const DataLoader: React.FC<DataLoaderProps> = ({
LoadingView = YourLoader, // Sie können Ihre eigene Ladekomponente verwenden
ErrorView = YourError, // Sie können Ihre eigene Fehlerkomponente verwenden
...restProps
}) => {
return <DataLoaderBase LoadingView={LoadingView} ErrorView={ErrorView} {...restProps} />;
};
4. Ihre erste Datenquelle definieren
import {skipContext} from '@gravity-ui/data-source';
// Ihre API-Funktion
import {fetchUser} from './api';
export const userDataSource = makePlainQueryDataSource({
// Schlüssel müssen eindeutig sein. Vielleicht sollten Sie einen Helfer zum Erstellen von Namen für Datenquellen erstellen
name: 'user',
// skipContext ist ein Helfer, um die ersten 2 Parameter der Funktion (context und fetchContext) zu überspringen
fetch: skipContext(fetchUser),
// Optional: Tags für fortgeschrittene Cache-Invalidierung generieren
tags: (params) => [`user:${params.userId}`, 'users'],
});
5. In Komponenten verwenden
import {useQueryData} from '@gravity-ui/data-source';
export const UserProfile: React.FC<{userId: number}> = ({userId}) => {
const {data, status, error, refetch} = useQueryData(userDataSource, {userId});
return (
<DataLoader status={status} error={error} errorAction={refetch}>
{data && <UserCard user={data} />}
</DataLoader>
);
};
Kernkonzepte
Datenquellentypen
Die Bibliothek bietet zwei Haupttypen von Datenquellen:
Plain Query Data Source
Für einfache Anfrage/Antwort-Muster:
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
Für Paginierung und unendliches Scrollen:
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;
},
});
Statusverwaltung
Die Bibliothek normalisiert Abfragestati in drei einfache Status:
loading- Aktuelles Laden der Daten. Dasselbe wieisLoadingin React Querysuccess- Daten verfügbar (kann mitidleübersprungen werden)error- Fehler beim Abrufen der Daten
Idle-Konzept
Die Bibliothek stellt ein spezielles idle-Symbol zur Verfügung, um die Abfrageausführung zu überspringen:
import {idle} from '@gravity-ui/data-source';
const UserProfile: React.FC<{userId?: number}> = ({userId}) => {
// Die Abfrage wird nicht ausgeführt, wenn userId nicht definiert ist
const {data, status} = useQueryData(userDataSource, userId ? {userId} : idle);
return (
<DataLoader status={status} error={null}>
{data && <UserCard user={data} />}
</DataLoader>
);
};
Wenn die Parameter gleich idle sind:
- Die Abfrage wird nicht ausgeführt
- Der Status bleibt
success - Die Daten bleiben
undefined - Die Komponente kann sicher ohne Ladeanzeige gerendert werden
Vorteile von idle:
- Typsicherheit - TypeScript leitet Typen für bedingte Parameter korrekt ab
- Performance - Vermeidet unnötige Serveranfragen
- Logikvereinfachung - Keine Notwendigkeit, zusätzlichen
enabled-Status zu verwalten - Konsistenz - Einheitlicher Ansatz für alle bedingten Abfragen
Dies ist besonders nützlich für bedingte Abfragen, wenn Sie Daten nur unter bestimmten Bedingungen laden möchten, während die Typsicherheit erhalten bleibt.
API-Referenz
Datenquellen erstellen
makePlainQueryDataSource(config)
Erstellt eine Plain Query Data Source für einfache Anfrage/Antwort-Muster.
Hier ist die übersetzte README-Datei:
const dataSource = makePlainQueryDataSource({
name: 'unique-name',
fetch: skipContext(fetchFunction),
transformParams: (params) => transformedRequest,
transformResponse: (response) => transformedData,
tags: (params) => ['tag1', 'tag2'],
options: {
staleTime: 60000,
retry: 3,
// ... weitere react-query Optionen
},
});
Parameter:
name- Eindeutiger Bezeichner für die Datenquellefetch- Funktion, die das eigentliche Datenabrufen durchführttransformParams(optional) - Transformiert Eingabeparameter vor der AnfragetransformResponse(optional) - Transformiert Antwortdatentags(optional) - Generiert Cache-Tags zur Invalidierungoptions(optional) - React Query Optionen
makeInfiniteQueryDataSource(config)
Erstellt eine Datenquelle für unendliche Abfragen für Paginierungs- und unendliche Scrollmuster.
const infiniteDataSource = makeInfiniteQueryDataSource({
name: 'infinite-data',
fetch: skipContext(fetchFunction),
next: (lastPage, allPages) => nextPageParam || undefined,
prev: (firstPage, allPages) => prevPageParam || undefined,
// ... weitere Optionen wie bei "plain"
});
Zusätzliche Parameter:
next- Funktion zur Bestimmung der Parameter für die nächste Seiteprev(optional) - Funktion zur Bestimmung der Parameter für die vorherige Seite
React Hooks
useQueryData(dataSource, params, options?)
Der Haupt-Hook zum Abrufen von Daten mit einer Datenquelle.
const {data, status, error, refetch, ...rest} = useQueryData(
userDataSource,
{userId: 123},
{
enabled: true,
refetchInterval: 30000,
},
);
Gibt zurück:
data- Die abgerufenen Datenstatus- Aktueller Status ('loading' | 'success' | 'error')error- Fehlerobjekt, wenn die Anfrage fehlgeschlagen istrefetch- Funktion zum manuellen erneuten Abrufen von Daten- Andere React Query Eigenschaften
useQueryResponses(responses)
Kombiniert mehrere Abfrageergebnisse in einen einzigen Zustand.
const user = useQueryData(userDataSource, {userId});
const posts = useQueryData(postsDataSource, {userId});
const {status, error, refetch, refetchErrored} = useQueryResponses([user, posts]);
Gibt zurück:
status- Kombinierter Status aller Abfragenerror- Erster aufgetretener Fehlerrefetch- Funktion zum erneuten Abrufen aller AbfragenrefetchErrored- Funktion zum erneuten Abrufen nur fehlgeschlagener Abfragen
useRefetchAll(states)
Erstellt eine Callback-Funktion zum erneuten Abrufen mehrerer Abfragen.
const refetchAll = useRefetchAll([user, posts, comments]);
// refetchAll() löst das erneute Abrufen aller Abfragen aus
useRefetchErrored(states)
Erstellt eine Callback-Funktion zum erneuten Abrufen nur fehlgeschlagener Abfragen.
const refetchErrored = useRefetchErrored([user, posts, comments]);
// refetchErrored() ruft nur Abfragen mit Fehlern erneut ab
useDataManager()
Gibt den DataManager aus dem Kontext zurück.
const dataManager = useDataManager();
await dataManager.invalidateTag('users');
useQueryContext()
Gibt den Abfragekontext zurück (zum Erstellen benutzerdefinierter Daten-Hooks basierend auf react-query).
React Komponenten
<DataLoader />
Komponente zur Handhabung von Ladezuständen und Fehlern.
<DataLoader
status={status}
error={error}
errorAction={refetch}
LoadingView={SpinnerComponent}
ErrorView={ErrorComponent}
loadingViewProps={{size: 'large'}}
errorViewProps={{showDetails: true}}
>
{data && <YourContent data={data} />}
</DataLoader>
Props:
status- Aktueller Ladezustanderror- FehlerobjekterrorAction- Funktion oder Aktionskonfiguration für den Fehler-RetryLoadingView- Komponente, die während des Ladens angezeigt wirdErrorView- Komponente, die bei einem Fehler angezeigt wirdloadingViewProps- An dieLoadingViewübergebene PropserrorViewProps- An dieErrorViewübergebene Props
<DataInfiniteLoader />
Spezialisierte Komponente für unendliche Abfragen.
<DataInfiniteLoader
status={status}
error={error}
hasNextPage={hasNextPage}
fetchNextPage={fetchNextPage}
isFetchingNextPage={isFetchingNextPage}
LoadingView={SpinnerComponent}
ErrorView={ErrorComponent}
MoreView={LoadMoreButton}
>
{data.map((item) => (
<Item key={item.id} data={item} />
))}
</DataInfiniteLoader>
Zusätzliche Props:
hasNextPage- Ob weitere Seiten verfügbar sindfetchNextPage- Funktion zum Abrufen der nächsten SeiteisFetchingNextPage- Ob die nächste Seite gerade abgerufen wirdMoreView- Komponente für den "Mehr laden"-Button
withDataManager(Component)
HOC, das den DataManager als Prop injiziert.
const MyComponent = withDataManager<Props>(({dataManager, ...props}) => {
// Die Komponente hat Zugriff auf dataManager
return <div>...</div>;
});
Datenverwaltung
ClientDataManager
Hauptklasse für die Datenverwaltung.
const dataManager = new ClientDataManager({
defaultOptions: {
queries: {
staleTime: 300000, // 5 Minuten
retry: 3,
refetchOnWindowFocus: false,
},
},
});
Methoden:
invalidateTag(tag, options?)
Invalidiert alle Abfragen mit einem bestimmten Tag.
await dataManager.invalidateTag('users');
await dataManager.invalidateTag('posts', {
repeat: {count: 3, interval: 1000}, // Retry der Invalidierung
});
invalidateTags(tags, options?)
Invalidiert Abfragen, die alle angegebenen Tags haben.
await dataManager.invalidateTags(['user', 'profile']);
invalidateSource(dataSource, options?)
Invalidiert alle Abfragen für eine Datenquelle.
await dataManager.invalidateSource(userDataSource);
invalidateParams(dataSource, params, options?)
Invalidiert eine bestimmte Abfrage mit exakten Parametern.
await dataManager.invalidateParams(userDataSource, {userId: 123});
resetSource(dataSource)
Setzt (leert) alle gecachten Daten für eine Datenquelle zurück.
await dataManager.resetSource(userDataSource);
resetParams(dataSource, params)
Setzt gecachte Daten für spezifische Parameter zurück.
await dataManager.resetParams(userDataSource, {userId: 123});
invalidateSourceTags(dataSource, params, options?)
Invalidiert Abfragen basierend auf Tags, die von einer Datenquelle generiert wurden.
await dataManager.invalidateSourceTags(userDataSource, {userId: 123});
Hilfsprogramme
skipContext(fetchFunction)
Hilfsprogramm zum Anpassen bestehender Fetch-Funktionen an die Datenquellen-Schnittstelle.
// Bestehende Funktion
async function fetchUser(params: {userId: number}) {
// ...
}
// Angepasst für Datenquelle
const dataSource = makePlainQueryDataSource({
name: 'user',
fetch: skipContext(fetchUser), // Überspringt Kontext- und fetchContext-Parameter
});
withCatch(fetchFunction, errorHandler)
Fügt standardisierte Fehlerbehandlung zu Fetch-Funktionen hinzu.
const safeFetch = withCatch(fetchUser, (error) => ({error: true, message: error.message}));
withCancellation(fetchFunction)
Fügt Unterstützung für die Abbruchfunktion zu Fetch-Funktionen hinzu.
const cancellableFetch = withCancellation(fetchFunction);
// Behandelt automatisch AbortSignal von React Query
getProgressiveRefetch(options)
Erstellt eine Funktion für progressive Refetch-Intervalle.
const progressiveRefetch = getProgressiveRefetch({
minInterval: 1000, // Start mit 1 Sekunde
maxInterval: 30000, // Maximal 30 Sekunden
multiplier: 2, // Verdoppelt sich jedes Mal
});
const dataSource = makePlainQueryDataSource({
name: 'data',
fetch: skipContext(fetchData),
options: {
refetchInterval: progressiveRefetch,
},
});
normalizeStatus(status, fetchStatus)
Konvertiert React Query-Status in DataLoader-Status.
const status = normalizeStatus('pending', 'fetching'); // 'loading'
Status- und Fehler-Dienstprogramme
// Kombinierten Status aus mehreren Zuständen abrufen
const status = getStatus([user, posts, comments]);
// Ersten Fehler aus mehreren Zuständen abrufen
const error = getError([user, posts, comments]);
// Mehrere Status zusammenführen
const combinedStatus = mergeStatuses(['loading', 'success', 'error']); // 'error'
// Prüfen, ob ein Query-Schlüssel ein Tag hat
const hasUserTag = hasTag(queryKey, 'users');
Dienstprogramme zur Schlüsselkomposition
// Cache-Schlüssel für eine Datenquelle zusammenstellen
const key = composeKey(userDataSource, {userId: 123});
// Vollständigen Schlüssel einschließlich Tags zusammenstellen
const fullKey = composeFullKey(userDataSource, {userId: 123});
Konstanten
import {idle} from '@gravity-ui/data-source';
// Spezielles Symbol zum Überspringen der Query-Ausführung
const params = shouldFetch ? {userId: 123} : idle;
// Typsichere Alternative zu enabled: false
// Anstatt:
const {data} = useQueryData(userDataSource, {userId: userId || ''}, {enabled: Boolean(userId)});
// Verwenden Sie:
const {data} = useQueryData(userDataSource, userId ? {userId} : idle);
// TypeScript leitet Typen für beide Zweige korrekt ab
Komposition von Query-Optionen
// React Query-Optionen für einfache Queries zusammenstellen
const plainOptions = composePlainQueryOptions(context, dataSource, params, options);
// React Query-Optionen für unendliche Queries zusammenstellen
const infiniteOptions = composeInfiniteQueryOptions(context, dataSource, params, options);
Hinweis: Diese Funktionen sind hauptsächlich für die interne Verwendung bei der Erstellung benutzerdefinierter Datenquellenimplementierungen bestimmt.
Fortgeschrittene Muster
Bedingte Queries mit Idle
Verwenden Sie idle, um bedingte Queries zu erstellen:
import {idle} from '@gravity-ui/data-source';
const ConditionalDataComponent: React.FC<{
userId?: number;
shouldLoadPosts: boolean;
}> = ({userId, shouldLoadPosts}) => {
// Benutzer nur laden, wenn userId definiert ist
const user = useQueryData(
userDataSource,
userId ? {userId} : idle
);
// Beiträge nur laden, wenn der Benutzer geladen ist und das Flag aktiviert ist
const posts = useQueryData(
userPostsDataSource,
user.data && shouldLoadPosts ? {userId: user.data.id} : idle
);
const combined = useQueryResponses([user, posts]);
return (
<DataLoader status={combined.status} error={combined.error}>
<div>
{user.data && <UserInfo user={user.data} />}
{posts.data && <UserPosts posts={posts.data} />}
</div>
</DataLoader>
);
};
Datentransformation
Transformieren Sie Anfrageparameter und Antwortdaten:
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),
});
Tag-basierte Cache-Invalidierung
Verwenden Sie Tags für ein ausgefeiltes Cache-Management:
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),
});
// Alle Daten für einen bestimmten Benutzer ungültig machen
await dataManager.invalidateTag('user:123');
// Alle benutzerbezogenen Daten ungültig machen
await dataManager.invalidateTag('users');
Fehlerbehandlung mit Typen
Erstellen Sie typsichere Fehlerbehandlung:
interface ApiError {
code: number;
message: string;
details?: Record<string, unknown>;
}
const ErrorView: React.FC<ErrorViewProps<ApiError>> = ({error, action}) => (
<div className="error">
<h3>Fehler {error?.code}</h3>
<p>{error?.message}</p>
{action && (
<button onClick={action.handler}>
{action.children || 'Erneut versuchen'}
</button>
)}
</div>
);
Unendliche Queries mit komplexer Paginierung
Behandeln Sie komplexe Paginierungsszenarien:
interface PaginationParams {
cursor?: string;
limit?: number;
filters?: Record<string, unknown>;
}
interface PaginatedResponse<T> {
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<DataItem>;
}),
next: (lastPage) => {
if (lastPage.hasMore && lastPage.nextCursor) {
return {cursor: lastPage.nextCursor, limit: 20};
}
return undefined;
},
});
Kombination mehrerer Datenquellen
Kombinieren Sie Daten aus mehreren Quellen:
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 (
<DataLoader
status={combined.status}
error={combined.error}
errorAction={combined.refetchErrored} // Nur fehlgeschlagene Anfragen erneut versuchen
LoadingView={ProfileSkeleton}
ErrorView={ProfileError}
>
{user && posts && followers && (
<div>
<UserInfo user={user.data} />
<UserPosts posts={posts.data} />
<UserFollowers followers={followers.data} />
</div>
)}
</DataLoader>
);
};
TypeScript-Unterstützung
Die Bibliothek wurde mit einem TypeScript-First-Ansatz entwickelt und bietet vollständige Typinferenz:
// Typen werden automatisch abgeleitet
const userDataSource = makePlainQueryDataSource({
name: 'user',
fetch: skipContext(async (params: {userId: number}): Promise<User> => {
// Rückgabetyp wird als User abgeleitet
}),
});
// Rückgabetyp des Hooks ist automatisch typisiert
const {data} = useQueryData(userDataSource, {userId: 123});
// data ist als User | undefined typisiert
Benutzerdefinierte Fehlertypen
Definieren und verwenden Sie benutzerdefinierte Fehlertypen:
interface ValidationError {
field: string;
message: string;
}
interface ApiError {
type: 'network' | 'validation' | 'server';
message: string;
validation?: ValidationError[];
}
const typedDataSource = makePlainQueryDataSource<
{id: number}, // Parametertyp
{id: number}, // Anfragetyp
ApiResponse, // Antworttyp
User, // Datentyp
ApiError // Fehlertyp
>({
name: 'typed-user',
fetch: skipContext(fetchUser),
});
Mitwirkung
Bitte lesen Sie CONTRIBUTING.md für Details zu unserem Verhaltenskodex und dem Prozess für das Einreichen von Pull-Anfragen.
Lizenz
MIT-Lizenz. Details finden Sie in der Datei LICENSE.