Bibliothèques / Data Source

Data Source

Un wrapper pour la récupération de données.

Data Source · npm version ci

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 utilisant idle).
  • 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 :

  1. Sécurité des Types - TypeScript infère correctement les types pour les paramètres conditionnels.
  2. Performance - Évite les requêtes serveur inutiles.
  3. Simplicité Logique - Pas besoin de gérer un état enabled supplémentaire.
  4. 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ées
  • fetch - Fonction qui effectue la récupération réelle des données
  • transformParams (optionnel) - Transforme les paramètres d'entrée avant la requête
  • transformResponse (optionnel) - Transforme les données de réponse
  • tags (optionnel) - Génère des tags de cache pour l'invalidation
  • options (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 suivante
  • prev (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ées
  • status - 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êtes
  • error - Première erreur rencontrée
  • refetch - Fonction pour relancer toutes les requêtes
  • refetchErrored - 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 actuel
  • error - Objet d'erreur
  • errorAction - Fonction ou configuration d'action pour la nouvelle tentative d'erreur
  • LoadingView - Composant à afficher pendant le chargement
  • ErrorView - Composant à afficher en cas d'erreur
  • loadingViewProps - Props passées à LoadingView
  • errorViewProps - 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 disponibles
  • fetchNextPage - Fonction pour récupérer la page suivante
  • isFetchingNextPage - Indique si la page suivante est en cours de récupération
  • MoreView - 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

Requêtes Conditionnelles avec idle

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.

À propos de la bibliothèque
Étoiles
27
Version
0.7.0
Dernière mise à jour
04.06.2025
Dépôt
github.com/gravity-ui/data-source
Licence
MIT License
Mainteneurs
Contributeurs