Skip to content

Repository files navigation

Data Source · npm versionci

Data Source is a simple wrapper around data fetching. It is a kind of "port" in clean architecture. It allows you to make wrappers for stuff around data fetching depending on your use cases. Data Source uses react-query under the hood.

Installation

npm install @gravity-ui/data-source @tanstack/react-query

@tanstack/react-query is a peer dependency.

Quick Start

1. Setup DataManager

First, create and provide a DataManager in your application:

importReactfrom'react';import{ClientDataManager,DataSourceProvider}from'@gravity-ui/data-source';constdataManager=newClientDataManager({defaultOptions: {queries: {staleTime: 5*60*1000,// 5 minutesretry: 3,},// ... other react-query options},});functionApp(){return(<DataSourceProviderdataManager={dataManager}><YourApplication/></DataSourceProvider>);}

2. Define Error Types and Wrappers

Define a type of error and make your constructors for data sources based on default constructors:

import{makePlainQueryDataSourceasmakePlainQueryDataSourceBase}from'@gravity-ui/data-source';exportinterfaceApiError{code: number;title: string;description?: string;}exportconstmakePlainQueryDataSource=<TParams,TRequest,TResponse,TData,TError=ApiError>(config: Omit<PlainQueryDataSource<TParams,TRequest,TResponse,TData,TError>,'type'>,): PlainQueryDataSource<TParams,TRequest,TResponse,TData,TError>=>{returnmakePlainQueryDataSourceBase(config);};

3. Create Custom DataLoader Component

Write a DataLoader component based on default to define your display of loading status and errors:

import{DataLoaderasDataLoaderBase,DataLoaderPropsasDataLoaderPropsBase,ErrorViewProps,}from'@gravity-ui/data-source';exportinterfaceDataLoaderPropsextendsOmit<DataLoaderPropsBase<ApiError>,'LoadingView'|'ErrorView'>{LoadingView?: ComponentType;ErrorView?: ComponentType<ErrorViewProps<ApiError>>;}exportconstDataLoader: React.FC<DataLoaderProps>=({
LoadingView =YourLoader,// You can use your own loader component
ErrorView =YourError,// You can use your own error component
...restProps})=>{return<DataLoaderBaseLoadingView={LoadingView}ErrorView={ErrorView}{...restProps}/>;};

4. Define Your First Data Source

import{skipContext}from'@gravity-ui/data-source';// Your API functionimport{fetchUser}from'./api';exportconstuserDataSource=makePlainQueryDataSource({// Keys have to be unique. Maybe you should create a helper for making names of data sourcesname: 'user',// skipContext is a helper to skip 2 first parameters in the function (context and fetchContext)fetch: skipContext(fetchUser),// Optional: generate tags for advanced cache invalidationtags: (params)=>[`user:${params.userId}`,'users'],});

5. Use in Components

import{useQueryData}from'@gravity-ui/data-source';exportconstUserProfile: React.FC<{userId: number}>=({userId})=>{const{data, status, error, refetch}=useQueryData(userDataSource,{userId});return(<DataLoaderstatus={status}error={error}errorAction={refetch}>{data&&<UserCarduser={data}/>}</DataLoader>);};

Core Concepts

Data Source Types

The library provides two main types of data sources:

Plain Query Data Source

For simple request/response patterns:

constuserDataSource=makePlainQueryDataSource({name: 'user',fetch: skipContext(async(params: {userId: number})=>{constresponse=awaitfetch(`/api/users/${params.userId}`);returnresponse.json();}),});

Infinite Query Data Source

For pagination and infinite scrolling:

constpostsDataSource=makeInfiniteQueryDataSource({name: 'posts',fetch: skipContext(async(params: {page: number;limit: number})=>{constresponse=awaitfetch(`/api/posts?page=${params.page}&limit=${params.limit}`);returnresponse.json();}),next: (lastPage,allPages)=>{if(lastPage.hasNext){return{page: allPages.length+1,limit: 20};}returnundefined;},});

Status Management

The library normalizes query states into three simple statuses:

  • loading - Actual data loading. The same as isLoading in React Query
  • success - Data available (may be skipped using idle)
  • error - Failed to fetch data

Idle Concept

The library provides a special idle symbol for skipping query execution:

import{idle}from'@gravity-ui/data-source';constUserProfile: React.FC<{userId?: number}>=({userId})=>{// Query won't execute if userId is not definedconst{data, status}=useQueryData(userDataSource,userId ? {userId} : idle);return(<DataLoaderstatus={status}error={null}>{data&&<UserCarduser={data}/>}</DataLoader>);};

When parameters equal idle:

  • Query doesn't execute
  • Status remains success
  • Data remains undefined
  • Component can safely render without loading

Benefits of idle:

  1. Type Safety - TypeScript correctly infers types for conditional parameters
  2. Performance - Avoids unnecessary server requests
  3. Logic Simplicity - No need to manage additional enabled state
  4. Consistency - Unified approach for all conditional queries

This is especially useful for conditional queries when you want to load data only under certain conditions while maintaining type safety.

API Reference

Creating Data Sources

makePlainQueryDataSource(config)

Creates a plain query data source for simple request/response patterns.

constdataSource=makePlainQueryDataSource({name: 'unique-name',fetch: skipContext(fetchFunction),transformParams: (params)=>transformedRequest,transformResponse: (response)=>transformedData,tags: (params)=>['tag1','tag2'],options: {staleTime: 60000,retry: 3,// ... other react-query options},});

Parameters:

  • name - Unique identifier for the data source
  • fetch - Function that performs the actual data fetching
  • transformParams (optional) - Transform input parameters before request
  • transformResponse (optional) - Transform response data
  • tags (optional) - Generate cache tags for invalidation
  • options (optional) - React Query options

makeInfiniteQueryDataSource(config)

Creates an infinite query data source for pagination and infinite scrolling patterns.

constinfiniteDataSource=makeInfiniteQueryDataSource({name: 'infinite-data',fetch: skipContext(fetchFunction),next: (lastPage,allPages)=>nextPageParam||undefined,prev: (firstPage,allPages)=>prevPageParam||undefined,// ... other options same as plain});

Additional Parameters:

  • next - Function to determine next page parameters
  • prev (optional) - Function to determine previous page parameters

React Hooks

useQueryData(dataSource, params, options?)

Main hook for fetching data with a data source.

const{data, status, error, refetch, ...rest}=useQueryData(userDataSource,{userId: 123},{enabled: true,refetchInterval: 30000,},);

Returns:

  • data - The fetched data
  • status - Current status ('loading' | 'success' | 'error')
  • error - Error object if request failed
  • refetch - Function to manually refetch data
  • Other React Query properties

useQueryResponses(responses)

Combines multiple query responses into a single state.

constuser=useQueryData(userDataSource,{userId});constposts=useQueryData(postsDataSource,{userId});const{status, error, refetch, refetchErrored}=useQueryResponses([user,posts]);

Returns:

  • status - Combined status of all queries
  • error - First error encountered
  • refetch - Function to refetch all queries
  • refetchErrored - Function to refetch only failed queries

useRefetchAll(states)

Creates a callback to refetch multiple queries.

constrefetchAll=useRefetchAll([user,posts,comments]);// refetchAll() will trigger refetch for all queries

useRefetchErrored(states)

Creates a callback to refetch only failed queries.

constrefetchErrored=useRefetchErrored([user,posts,comments]);// refetchErrored() will only refetch queries with errors

useDataManager()

Returns the DataManager from context.

constdataManager=useDataManager();awaitdataManager.invalidateTag('users');

useQueryContext()

Returns the query context (for building custom data hooks base on react-query).

React Components

<DataLoader />

Component for handling loading states and errors.

<DataLoaderstatus={status}error={error}errorAction={refetch}LoadingView={SpinnerComponent}ErrorView={ErrorComponent}loadingViewProps={{size: 'large'}}errorViewProps={{showDetails: true}}>{data&&<YourContentdata={data}/>}</DataLoader>

Props:

  • status - Current loading status
  • error - Error object
  • errorAction - Function or action config for error retry
  • LoadingView - Component to show during loading
  • ErrorView - Component to show on error
  • loadingViewProps - Props passed to LoadingView
  • errorViewProps - Props passed to ErrorView

<DataInfiniteLoader />

Specialized component for infinite queries.

<DataInfiniteLoaderstatus={status}error={error}hasNextPage={hasNextPage}fetchNextPage={fetchNextPage}isFetchingNextPage={isFetchingNextPage}LoadingView={SpinnerComponent}ErrorView={ErrorComponent}MoreView={LoadMoreButton}>{data.map((item)=>(<Itemkey={item.id}data={item}/>))}</DataInfiniteLoader>

Additional Props:

  • hasNextPage - Whether more pages are available
  • fetchNextPage - Function to fetch next page
  • isFetchingNextPage - Whether next page is being fetched
  • MoreView - Component for "load more" button

withDataManager(Component)

HOC that injects DataManager as a prop.

constMyComponent=withDataManager<Props>(({dataManager, ...props})=>{// Component has access to dataManagerreturn<div>...</div>;});

Data Management

ClientDataManager

Main class for data management.

constdataManager=newClientDataManager({defaultOptions: {queries: {staleTime: 300000,// 5 minutesretry: 3,refetchOnWindowFocus: false,},},});

Methods:

invalidateTag(tag, options?)

Invalidate all queries with a specific tag.

awaitdataManager.invalidateTag('users');awaitdataManager.invalidateTag('posts',{repeat: {count: 3,interval: 1000},// Retry invalidation});
invalidateTags(tags, options?)

Invalidate queries that have all specified tags.

awaitdataManager.invalidateTags(['user','profile']);
invalidateSource(dataSource, options?)

Invalidate all queries for a data source.

awaitdataManager.invalidateSource(userDataSource);
invalidateParams(dataSource, params, options?)

Invalidate a specific query with exact parameters.

awaitdataManager.invalidateParams(userDataSource,{userId: 123});
resetSource(dataSource)

Reset (clear) all cached data for a data source.

awaitdataManager.resetSource(userDataSource);
resetParams(dataSource, params)

Reset cached data for specific parameters.

awaitdataManager.resetParams(userDataSource,{userId: 123});
invalidateSourceTags(dataSource, params, options?)

Invalidate queries based on tags generated by a data source.

awaitdataManager.invalidateSourceTags(userDataSource,{userId: 123});

Utilities

skipContext(fetchFunction)

Utility to adapt existing fetch functions to data source interface.

// Existing functionasyncfunctionfetchUser(params: {userId: number}){// ...}// Adapted for data sourceconstdataSource=makePlainQueryDataSource({name: 'user',fetch: skipContext(fetchUser),// Skips context and fetchContext params});

withCatch(fetchFunction, errorHandler)

Adds standardized error handling to fetch functions.

constsafeFetch=withCatch(fetchUser,(error)=>({error: true,message: error.message}));

withCancellation(fetchFunction)

Adds cancellation support to fetch functions.

constcancellableFetch=withCancellation(fetchFunction);// Automatically handles AbortSignal from React Query

getProgressiveRefetch(options)

Creates a progressive refetch interval function.

constprogressiveRefetch=getProgressiveRefetch({minInterval: 1000,// Start with 1 secondmaxInterval: 30000,// Max 30 secondsmultiplier: 2,// Double each time});constdataSource=makePlainQueryDataSource({name: 'data',fetch: skipContext(fetchData),options: {refetchInterval: progressiveRefetch,},});

normalizeStatus(status, fetchStatus)

Converts React Query statuses to DataLoader status.

conststatus=normalizeStatus('pending','fetching');// 'loading'

Status and Error Utilities

// Get combined status from multiple statesconststatus=getStatus([user,posts,comments]);// Get first error from multiple statesconsterror=getError([user,posts,comments]);// Merge multiple statusesconstcombinedStatus=mergeStatuses(['loading','success','error']);// 'error'// Check if query key has a tagconsthasUserTag=hasTag(queryKey,'users');

Key Composition Utilities

// Compose cache key for a data sourceconstkey=composeKey(userDataSource,{userId: 123});// Compose full key including tagsconstfullKey=composeFullKey(userDataSource,{userId: 123});

Constants

import{idle}from'@gravity-ui/data-source';// Special symbol for skipping query executionconstparams=shouldFetch ? {userId: 123} : idle;// Type-safe alternative to enabled: false// Instead of:const{data}=useQueryData(userDataSource,{userId: userId||''},{enabled: Boolean(userId)});// Use:const{data}=useQueryData(userDataSource,userId ? {userId} : idle);// TypeScript correctly infers types for both branches

Query Options Composition

// Compose React Query options for plain queriesconstplainOptions=composePlainQueryOptions(context,dataSource,params,options);// Compose React Query options for infinite queriesconstinfiniteOptions=composeInfiniteQueryOptions(context,dataSource,params,options);

Note: These functions are primarily for internal use when creating custom data source implementations.

Advanced Patterns

Conditional Queries with Idle

Use idle to create conditional queries:

import{idle}from'@gravity-ui/data-source';constConditionalDataComponent: React.FC<{userId?: number;shouldLoadPosts: boolean;}>=({userId, shouldLoadPosts})=>{// Load user only if userId is definedconstuser=useQueryData(userDataSource,userId ? {userId} : idle);// Load posts only if user is loaded and flag is enabledconstposts=useQueryData(userPostsDataSource,user.data&&shouldLoadPosts ? {userId: user.data.id} : idle);constcombined=useQueryResponses([user,posts]);return(<DataLoaderstatus={combined.status}error={combined.error}><div>{user.data&&<UserInfouser={user.data}/>}{posts.data&&<UserPostsposts={posts.data}/>}</div></DataLoader>);};

Data Transformation

Transform request parameters and response data:

constapiDataSource=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-Based Cache Invalidation

Use tags for sophisticated cache management:

constuserDataSource=makePlainQueryDataSource({name: 'user',tags: (params)=>[`user:${params.userId}`,'users','profiles'],fetch: skipContext(fetchUser),});constuserPostsDataSource=makePlainQueryDataSource({name: 'user-posts',tags: (params)=>[`user:${params.userId}`,'posts'],fetch: skipContext(fetchUserPosts),});// Invalidate all data for specific userawaitdataManager.invalidateTag('user:123');// Invalidate all user-related dataawaitdataManager.invalidateTag('users');

Error Handling with Types

Create type-safe error handling:

interfaceApiError{code: number;message: string;details?: Record<string,unknown>;}constErrorView: React.FC<ErrorViewProps<ApiError>>=({error, action})=>(<divclassName="error"><h3>Error{error?.code}</h3><p>{error?.message}</p>{action&&(<buttononClick={action.handler}>{action.children||'Retry'}</button>)}</div>);

Infinite Queries with Complex Pagination

Handle complex pagination scenarios:

interfacePaginationParams{cursor?: string;limit?: number;filters?: Record<string,unknown>;}interfacePaginatedResponse<T>{data: T[];nextCursor?: string;hasMore: boolean;}constinfiniteDataSource=makeInfiniteQueryDataSource({name: 'paginated-data',fetch: skipContext(async(params: PaginationParams)=>{constresponse=awaitfetch(`/api/data?${newURLSearchParams(params)}`);returnresponse.json()asPaginatedResponse<DataItem>;}),next: (lastPage)=>{if(lastPage.hasMore&&lastPage.nextCursor){return{cursor: lastPage.nextCursor,limit: 20};}returnundefined;},});

Combining Multiple Data Sources

Combine data from multiple sources:

constUserProfile: React.FC<{userId: number}>=({userId})=>{constuser=useQueryData(userDataSource,{userId});constposts=useQueryData(userPostsDataSource,{userId});constfollowers=useQueryData(userFollowersDataSource,{userId});constcombined=useQueryResponses([user,posts,followers]);return(<DataLoaderstatus={combined.status}error={combined.error}errorAction={combined.refetchErrored}// Only retry failed requestsLoadingView={ProfileSkeleton}ErrorView={ProfileError}>{user&&posts&&followers&&(<div><UserInfouser={user.data}/><UserPostsposts={posts.data}/><UserFollowersfollowers={followers.data}/></div>)}</DataLoader>);};

TypeScript Support

The library is built with TypeScript-first approach and provides full type inference:

// Types are automatically inferredconstuserDataSource=makePlainQueryDataSource({name: 'user',fetch: skipContext(async(params: {userId: number}): Promise<User>=>{// Return type is inferred as User}),});// Hook return type is automatically typedconst{data}=useQueryData(userDataSource,{userId: 123});// data is typed as User | undefined

Custom Error Types

Define and use custom error types:

interfaceValidationError{field: string;message: string;}interfaceApiError{type: 'network'|'validation'|'server';message: string;validation?: ValidationError[];}consttypedDataSource=makePlainQueryDataSource<{id: number},// Params type{id: number},// Request typeApiResponse,// Response typeUser,// Data typeApiError// Error type>({name: 'typed-user',fetch: skipContext(fetchUser),});

Contributing

Please read CONTRIBUTING.md for details on our code of conduct and the process for submitting pull requests.

License

MIT License. See LICENSE file for details.

About

A wrapper around data fetching

Topics

Resources

Contributing

Stars

30 stars

Watchers

1 watching

Forks

Releases

Used by

Contributors

Languages