Data Source
Data Source est un simple wrapper pour la récupération de données. C'est une sorte de "port" dans l'architecture propre. Il vous permet de créer des wrappers pour des éléments liés à la récupération de données, en fonction de vos cas d'utilisation. Data Source utilise react-query en arrière-plan.
Installation
npm install @gravity-ui/data-source @tanstack/react-query
@tanstack/react-query
est une dépendance pair.
Démarrage Rapide
1. Configuration du DataManager
Tout d'abord, créez et fournissez un DataManager
dans votre application :
import React from 'react';
import {ClientDataManager, DataManagerContext} from '@gravity-ui/data-source';
const dataManager = new ClientDataManager({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 minutes
retry: 3,
},
// ... autres options react-query
},
});
function App() {
return (
<DataManagerContext.Provider value={dataManager}>
<YourApplication />
</DataManagerContext.Provider>
);
}
2. Définition des Types d'Erreurs et des Wrappers
Définissez un type d'erreur et créez vos constructeurs pour les sources de données en vous basant sur les constructeurs par défaut :
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. Création d'un Composant DataLoader Personnalisé
Écrivez un composant DataLoader
basé sur le composant par défaut pour définir l'affichage de l'état de chargement et des erreurs :
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, // Vous pouvez utiliser votre propre composant de chargement
ErrorView = YourError, // Vous pouvez utiliser votre propre composant d'erreur
...restProps
}) => {
return <DataLoaderBase LoadingView={LoadingView} ErrorView={ErrorView} {...restProps} />;
};
4. Définition de Votre Première Source de Données
import {skipContext} from '@gravity-ui/data-source';
// Votre fonction API
import {fetchUser} from './api';
export const userDataSource = makePlainQueryDataSource({
// Les clés doivent être uniques. Peut-être devriez-vous créer un utilitaire pour nommer les sources de données
name: 'user',
// skipContext est un utilitaire pour ignorer les 2 premiers paramètres de la fonction (context et fetchContext)
fetch: skipContext(fetchUser),
// Optionnel : générer des tags pour une invalidation de cache avancée
tags: (params) => [`user:${params.userId}`, 'users'],
});
5. Utilisation dans les Composants
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>
);
};
Concepts Clés
Types de Sources de Données
La bibliothèque fournit deux types principaux de sources de données :
Source de Données de Requête Simple (Plain Query Data Source)
Pour les modèles simples de requête/réponse :
const userDataSource = makePlainQueryDataSource({
name: 'user',
fetch: skipContext(async (params: {userId: number}) => {
const response = await fetch(`/api/users/${params.userId}`);
return response.json();
}),
});
Source de Données de Requête Infinie (Infinite Query Data Source)
Pour la pagination et le défilement infini :
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;
},
});
Gestion des États
La bibliothèque normalise les états des requêtes en trois états simples :
loading
- Chargement effectif des données. Identique àisLoading
dans React Query.success
- Données disponibles (peut être ignoré en utilisantidle
).error
- Échec de la récupération des données.
Concept d'Idle
La bibliothèque fournit un symbole spécial idle
pour ignorer l'exécution de la requête :
import {idle} from '@gravity-ui/data-source';
const UserProfile: React.FC<{userId?: number}> = ({userId}) => {
// La requête ne s'exécutera pas si userId n'est pas défini
const {data, status} = useQueryData(userDataSource, userId ? {userId} : idle);
return (
<DataLoader status={status} error={null}>
{data && <UserCard user={data} />}
</DataLoader>
);
};
Lorsque les paramètres sont égaux à idle
:
- La requête ne s'exécute pas.
- Le statut reste
success
. - Les données restent
undefined
. - Le composant peut être rendu en toute sécurité sans chargement.
Avantages de idle
:
- Sécurité des Types - TypeScript infère correctement les types pour les paramètres conditionnels.
- Performance - Évite les requêtes serveur inutiles.
- Simplicité Logique - Pas besoin de gérer un état
enabled
supplémentaire. - Cohérence - Approche unifiée pour toutes les requêtes conditionnelles.
Ceci est particulièrement utile pour les requêtes conditionnelles lorsque vous souhaitez charger des données uniquement sous certaines conditions tout en maintenant la sécurité des types.
Référence API
Création de Sources de Données
makePlainQueryDataSource(config)
Crée une source de données de requête simple pour les modèles de requête/réponse.
<p>Options de langue :
<a href="/README.html">English</a> |
<a href="/README.fr.html">Français</a>
</p>
const dataSource = makePlainQueryDataSource({
name: 'unique-name',
fetch: skipContext(fetchFunction),
transformParams: (params) => transformedRequest,
transformResponse: (response) => transformedData,
tags: (params) => ['tag1', 'tag2'],
options: {
staleTime: 60000,
retry: 3,
// ... autres options react-query
},
});
Paramètres :
name
- Identifiant unique pour la source de donnéesfetch
- Fonction qui effectue la récupération réelle des donnéestransformParams
(optionnel) - Transforme les paramètres d'entrée avant la requêtetransformResponse
(optionnel) - Transforme les données de réponsetags
(optionnel) - Génère des tags de cache pour l'invalidationoptions
(optionnel) - Options React Query
makeInfiniteQueryDataSource(config)
Crée une source de données de requête infinie pour les modèles de pagination et de défilement infini.
const infiniteDataSource = makeInfiniteQueryDataSource({
name: 'infinite-data',
fetch: skipContext(fetchFunction),
next: (lastPage, allPages) => nextPageParam || undefined,
prev: (firstPage, allPages) => prevPageParam || undefined,
// ... autres options identiques à plain
});
Paramètres supplémentaires :
next
- Fonction pour déterminer les paramètres de la page suivanteprev
(optionnel) - Fonction pour déterminer les paramètres de la page précédente
Hooks React
useQueryData(dataSource, params, options?)
Hook principal pour récupérer des données avec une source de données.
const {data, status, error, refetch, ...rest} = useQueryData(
userDataSource,
{userId: 123},
{
enabled: true,
refetchInterval: 30000,
},
);
Retourne :
data
- Les données récupéréesstatus
- Statut actuel ('loading' | 'success' | 'error')error
- Objet d'erreur si la requête a échouérefetch
- Fonction pour relancer manuellement la récupération des données- Autres propriétés React Query
useQueryResponses(responses)
Combine plusieurs réponses de requête en un seul état.
const user = useQueryData(userDataSource, {userId});
const posts = useQueryData(postsDataSource, {userId});
const {status, error, refetch, refetchErrored} = useQueryResponses([user, posts]);
Retourne :
status
- Statut combiné de toutes les requêteserror
- Première erreur rencontréerefetch
- Fonction pour relancer toutes les requêtesrefetchErrored
- Fonction pour relancer uniquement les requêtes échouées
useRefetchAll(states)
Crée une fonction de rappel pour relancer plusieurs requêtes.
const refetchAll = useRefetchAll([user, posts, comments]);
// refetchAll() déclenchera la relance de toutes les requêtes
useRefetchErrored(states)
Crée une fonction de rappel pour relancer uniquement les requêtes échouées.
const refetchErrored = useRefetchErrored([user, posts, comments]);
// refetchErrored() ne relancera que les requêtes ayant des erreurs
useDataManager()
Retourne le DataManager du contexte.
const dataManager = useDataManager();
await dataManager.invalidateTag('users');
useQueryContext()
Retourne le contexte de requête (pour construire des hooks de données personnalisés basés sur react-query).
Composants React
<DataLoader />
Composant pour gérer les états de chargement et les erreurs.
<DataLoader
status={status}
error={error}
errorAction={refetch}
LoadingView={SpinnerComponent}
ErrorView={ErrorComponent}
loadingViewProps={{size: 'large'}}
errorViewProps={{showDetails: true}}
>
{data && <YourContent data={data} />}
</DataLoader>
Props :
status
- Statut de chargement actuelerror
- Objet d'erreurerrorAction
- Fonction ou configuration d'action pour la nouvelle tentative d'erreurLoadingView
- Composant à afficher pendant le chargementErrorView
- Composant à afficher en cas d'erreurloadingViewProps
- Props passées à LoadingViewerrorViewProps
- Props passées à ErrorView
<DataInfiniteLoader />
Composant spécialisé pour les requêtes infinies.
<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>
Props supplémentaires :
hasNextPage
- Indique si d'autres pages sont disponiblesfetchNextPage
- Fonction pour récupérer la page suivanteisFetchingNextPage
- Indique si la page suivante est en cours de récupérationMoreView
- Composant pour le bouton "charger plus"
withDataManager(Component)
HOC qui injecte DataManager en tant que prop.
const MyComponent = withDataManager<Props>(({dataManager, ...props}) => {
// Le composant a accès à dataManager
return <div>...</div>;
});
Gestion des données
ClientDataManager
Classe principale pour la gestion des données.
const dataManager = new ClientDataManager({
defaultOptions: {
queries: {
staleTime: 300000, // 5 minutes
retry: 3,
refetchOnWindowFocus: false,
},
},
});
Méthodes :
invalidateTag(tag, options?)
Invalide toutes les requêtes avec un tag spécifique.
await dataManager.invalidateTag('users');
await dataManager.invalidateTag('posts', {
repeat: {count: 3, interval: 1000}, // Nouvelle tentative d'invalidation
});
invalidateTags(tags, options?)
Invalide les requêtes qui ont tous les tags spécifiés.
await dataManager.invalidateTags(['user', 'profile']);
invalidateSource(dataSource, options?)
Invalide toutes les requêtes pour une source de données.
await dataManager.invalidateSource(userDataSource);
invalidateParams(dataSource, params, options?)
Invalide une requête spécifique avec des paramètres exacts.
await dataManager.invalidateParams(userDataSource, {userId: 123});
resetSource(dataSource)
Réinitialise (efface) toutes les données mises en cache pour une source de données.
await dataManager.resetSource(userDataSource);
resetParams(dataSource, params)
Réinitialise les données mises en cache pour des paramètres spécifiques.
await dataManager.resetParams(userDataSource, {userId: 123});
invalidateSourceTags(dataSource, params, options?)
Invalide les requêtes en fonction des tags générés par une source de données.
await dataManager.invalidateSourceTags(userDataSource, {userId: 123});
Utilitaires
skipContext(fetchFunction)
Utilité pour adapter les fonctions fetch existantes à l'interface de source de données.
// Fonction existante
async function fetchUser(params: {userId: number}) {
// ...
}
// Adaptée pour la source de données
const dataSource = makePlainQueryDataSource({
name: 'user',
fetch: skipContext(fetchUser), // Ignore le contexte et les paramètres de fetchContext
});
withCatch(fetchFunction, errorHandler)
<div class="language-selector">
<a href="/en/readme.html">English</a>
<a href="/fr/readme.html">Français</a>
</div>
Ajoute une gestion d'erreurs standardisée aux fonctions fetch
.
const safeFetch = withCatch(fetchUser, (error) => ({error: true, message: error.message}));
withCancellation(fetchFunction)
Ajoute la prise en charge de l'annulation aux fonctions fetch
.
const cancellableFetch = withCancellation(fetchFunction);
// Gère automatiquement AbortSignal de React Query
getProgressiveRefetch(options)
Crée une fonction d'intervalle de rafraîchissement progressif.
const progressiveRefetch = getProgressiveRefetch({
minInterval: 1000, // Commence à 1 seconde
maxInterval: 30000, // Max 30 secondes
multiplier: 2, // Double à chaque fois
});
const dataSource = makePlainQueryDataSource({
name: 'data',
fetch: skipContext(fetchData),
options: {
refetchInterval: progressiveRefetch,
},
});
normalizeStatus(status, fetchStatus)
Convertit les statuts de React Query en statuts DataLoader.
const status = normalizeStatus('pending', 'fetching'); // 'loading'
Utilitaires de Statut et d'Erreur
// Obtient le statut combiné à partir de plusieurs états
const status = getStatus([user, posts, comments]);
// Obtient la première erreur à partir de plusieurs états
const error = getError([user, posts, comments]);
// Fusionne plusieurs statuts
const combinedStatus = mergeStatuses(['loading', 'success', 'error']); // 'error'
// Vérifie si une clé de requête a un tag
const hasUserTag = hasTag(queryKey, 'users');
Utilitaires de Composition de Clés
// Compose la clé de cache pour une source de données
const key = composeKey(userDataSource, {userId: 123});
// Compose la clé complète incluant les tags
const fullKey = composeFullKey(userDataSource, {userId: 123});
Constantes
import {idle} from '@gravity-ui/data-source';
// Symbole spécial pour ignorer l'exécution de la requête
const params = shouldFetch ? {userId: 123} : idle;
// Alternative typée à enabled: false
// Au lieu de :
const {data} = useQueryData(userDataSource, {userId: userId || ''}, {enabled: Boolean(userId)});
// Utilisez :
const {data} = useQueryData(userDataSource, userId ? {userId} : idle);
// TypeScript infère correctement les types pour les deux branches
Composition des Options de Requête
// Compose les options React Query pour les requêtes simples
const plainOptions = composePlainQueryOptions(context, dataSource, params, options);
// Compose les options React Query pour les requêtes infinies
const infiniteOptions = composeInfiniteQueryOptions(context, dataSource, params, options);
Note : Ces fonctions sont principalement destinées à un usage interne lors de la création d'implémentations de sources de données personnalisées.
Modèles Avancés
idle
Requêtes Conditionnelles avec Utilisez idle
pour créer des requêtes conditionnelles :
import {idle} from '@gravity-ui/data-source';
const ConditionalDataComponent: React.FC<{
userId?: number;
shouldLoadPosts: boolean;
}> = ({userId, shouldLoadPosts}) => {
// Charge l'utilisateur uniquement si userId est défini
const user = useQueryData(
userDataSource,
userId ? {userId} : idle
);
// Charge les posts uniquement si l'utilisateur est chargé et que le drapeau est activé
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>
);
};
Transformation des Données
Transformez les paramètres de requête et les données de réponse :
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),
});
Invalidation de Cache Basée sur les Tags
Utilisez des tags pour une gestion sophistiquée du cache :
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),
});
// Invalide toutes les données pour un utilisateur spécifique
await dataManager.invalidateTag('user:123');
// Invalide toutes les données liées à l'utilisateur
await dataManager.invalidateTag('users');
Gestion des Erreurs avec des Types
Créez une gestion d'erreurs typée :
interface ApiError {
code: number;
message: string;
details?: Record<string, unknown>;
}
const ErrorView: React.FC<ErrorViewProps<ApiError>> = ({error, action}) => (
<div className="error">
<h3>Erreur {error?.code}</h3>
<p>{error?.message}</p>
{action && (
<button onClick={action.handler}>
{action.children || 'Réessayer'}
</button>
)}
</div>
);
Requêtes Infinies avec Pagination Complexe
Gérez des scénarios de pagination complexes :
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;
},
});
Combinaison de Plusieurs Sources de Données
Combinez les données de plusieurs sources :
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]);
<p>
<a href="README.md">English</a> |
<a href="README.fr.md">Français</a>
</p>
@gravity/data-source
Ce package fournit un ensemble d'outils pour gérer les états de chargement, d'erreur et de données de vos requêtes asynchrones. Il est conçu pour être utilisé avec des frameworks comme React, mais peut être adapté à d'autres environnements.
Fonctionnalités
- Gestion d'état simplifiée : Gère automatiquement les états de chargement, de succès et d'erreur pour vos requêtes.
- Composants réutilisables : Fournit des composants pour afficher des vues de chargement, d'erreur et de données.
- Intégration facile : S'intègre facilement dans votre application existante.
- Support TypeScript : Construit avec une approche "TypeScript-first" pour une meilleure expérience de développement.
Installation
npm install @gravity/data-source
# ou
yarn add @gravity/data-source
Utilisation
Voici un exemple d'utilisation de DataLoader
pour afficher des informations utilisateur, des publications et des abonnés :
import React from 'react';
import { DataLoader } from '@gravity/data-source';
import { ProfileSkeleton, ProfileError } from './views'; // Vos composants de vue
import { UserInfo, UserPosts, UserFollowers } from './components'; // Vos composants de données
const UserProfile = ({ combined }) => {
const { user, posts, followers } = combined;
return (
<DataLoader
status={combined.status}
error={combined.error}
errorAction={combined.refetchErrored} // Seulement pour retenter les requêtes échouées
LoadingView={ProfileSkeleton}
ErrorView={ProfileError}
>
{user && posts && followers && (
<div>
<UserInfo user={user.data} />
<UserPosts posts={posts.data} />
<UserFollowers followers={followers.data} />
</div>
)}
</DataLoader>
);
};
Support TypeScript
La bibliothèque est construite avec une approche "TypeScript-first" et offre une inférence de type complète :
// Les types sont automatiquement inférés
const userDataSource = makePlainQueryDataSource({
name: 'user',
fetch: skipContext(async (params: {userId: number}): Promise<User> => {
// Le type de retour est inféré comme User
}),
});
// Le type de retour du hook est automatiquement typé
const {data} = useQueryData(userDataSource, {userId: 123});
// data est typé comme User | undefined
Types d'erreur personnalisés
Définissez et utilisez des types d'erreur personnalisés :
interface ValidationError {
field: string;
message: string;
}
interface ApiError {
type: 'network' | 'validation' | 'server';
message: string;
validation?: ValidationError[];
}
const typedDataSource = makePlainQueryDataSource<
{id: number}, // Type des paramètres
{id: number}, // Type de la requête
ApiResponse, // Type de la réponse
User, // Type des données
ApiError // Type de l'erreur
>({
name: 'typed-user',
fetch: skipContext(fetchUser),
});
Contribution
Veuillez lire CONTRIBUTING.md pour plus de détails sur notre code de conduite et le processus de soumission des pull requests.
Licence
MIT License. Voir le fichier LICENSE pour plus de détails.