@gravity-ui/data-source 0.7.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +752 -15
- package/build/cjs/core/index.d.ts +1 -0
- package/build/cjs/core/index.js.map +1 -1
- package/build/cjs/core/types/DataManager.d.ts +5 -0
- package/build/cjs/core/types/DataManager.js.map +1 -1
- package/build/cjs/core/types/Normalizer.d.ts +29 -0
- package/build/cjs/core/types/Normalizer.js +6 -0
- package/build/cjs/core/types/Normalizer.js.map +1 -0
- package/build/cjs/react/DataManagerProvider.d.ts +7 -0
- package/build/cjs/react/DataManagerProvider.js +19 -0
- package/build/cjs/react/DataManagerProvider.js.map +1 -0
- package/build/cjs/react/__tests__/DataManagerContext.test.js +7 -6
- package/build/cjs/react/__tests__/DataManagerContext.test.js.map +1 -1
- package/build/cjs/react/__tests__/withDataManager.test.js +3 -0
- package/build/cjs/react/__tests__/withDataManager.test.js.map +1 -1
- package/build/cjs/react/index.d.ts +2 -0
- package/build/cjs/react/index.js +8 -0
- package/build/cjs/react/index.js.map +1 -1
- package/build/cjs/react-query/ClientDataManager.d.ts +16 -3
- package/build/cjs/react-query/ClientDataManager.js +158 -13
- package/build/cjs/react-query/ClientDataManager.js.map +1 -1
- package/build/cjs/react-query/DataSourceProvider.d.ts +7 -0
- package/build/cjs/react-query/DataSourceProvider.js +35 -0
- package/build/cjs/react-query/DataSourceProvider.js.map +1 -0
- package/build/cjs/react-query/__tests__/createQueryNormalizer.test.js +177 -0
- package/build/cjs/react-query/__tests__/createQueryNormalizer.test.js.map +1 -0
- package/build/cjs/react-query/__tests__/normalizationEdgeCases.test.js +100 -0
- package/build/cjs/react-query/__tests__/normalizationEdgeCases.test.js.map +1 -0
- package/build/cjs/react-query/__tests__/subscriptions.test.js +1180 -0
- package/build/cjs/react-query/__tests__/subscriptions.test.js.map +1 -0
- package/build/cjs/react-query/__tests__/threeLevelIntegration.test.js +659 -0
- package/build/cjs/react-query/__tests__/threeLevelIntegration.test.js.map +1 -0
- package/build/cjs/react-query/__tests__/updateQueriesFromMutationData.test.js +229 -0
- package/build/cjs/react-query/__tests__/updateQueriesFromMutationData.test.js.map +1 -0
- package/build/cjs/react-query/hooks/__tests__/useQueryData.refetch.test.js +195 -0
- package/build/cjs/react-query/hooks/__tests__/useQueryData.refetch.test.js.map +1 -0
- package/build/cjs/react-query/impl/infinite/hooks.js +4 -1
- package/build/cjs/react-query/impl/infinite/hooks.js.map +1 -1
- package/build/cjs/react-query/impl/infinite/types.d.ts +2 -2
- package/build/cjs/react-query/impl/infinite/types.js.map +1 -1
- package/build/cjs/react-query/impl/infinite/utils.js +6 -1
- package/build/cjs/react-query/impl/infinite/utils.js.map +1 -1
- package/build/cjs/react-query/impl/plain/hooks.js +4 -1
- package/build/cjs/react-query/impl/plain/hooks.js.map +1 -1
- package/build/cjs/react-query/impl/plain/types.d.ts +2 -2
- package/build/cjs/react-query/impl/plain/types.js.map +1 -1
- package/build/cjs/react-query/impl/plain/utils.js +6 -1
- package/build/cjs/react-query/impl/plain/utils.js.map +1 -1
- package/build/cjs/react-query/index.d.ts +2 -0
- package/build/cjs/react-query/index.js +7 -0
- package/build/cjs/react-query/index.js.map +1 -1
- package/build/cjs/react-query/types/normalizer.d.ts +21 -0
- package/build/cjs/react-query/types/normalizer.js +6 -0
- package/build/cjs/react-query/types/normalizer.js.map +1 -0
- package/build/cjs/react-query/types/options.d.ts +12 -0
- package/build/cjs/react-query/types/options.js.map +1 -1
- package/build/cjs/react-query/types/query-meta.d.ts +12 -0
- package/build/cjs/react-query/utils/__tests__/checkMutationObjectsKeys.test.js +295 -0
- package/build/cjs/react-query/utils/__tests__/checkMutationObjectsKeys.test.js.map +1 -0
- package/build/cjs/react-query/utils/checkMutationObjectsKeys.d.ts +17 -0
- package/build/cjs/react-query/utils/checkMutationObjectsKeys.js +88 -0
- package/build/cjs/react-query/utils/checkMutationObjectsKeys.js.map +1 -0
- package/build/cjs/react-query/utils/normalize.d.ts +22 -0
- package/build/cjs/react-query/utils/normalize.js +150 -0
- package/build/cjs/react-query/utils/normalize.js.map +1 -0
- package/build/cjs/react-query/utils/parseQueryKey.d.ts +2 -0
- package/build/cjs/react-query/utils/parseQueryKey.js +10 -0
- package/build/cjs/react-query/utils/parseQueryKey.js.map +1 -0
- package/build/cjs/react-query/utils/warn.d.ts +1 -0
- package/build/cjs/react-query/utils/warn.js +15 -0
- package/build/cjs/react-query/utils/warn.js.map +1 -0
- package/build/cjs/react-query/utils/warnDisabledRefetch.d.ts +1 -0
- package/build/cjs/react-query/utils/warnDisabledRefetch.js +11 -0
- package/build/cjs/react-query/utils/warnDisabledRefetch.js.map +1 -0
- package/build/esm/core/index.d.ts +1 -0
- package/build/esm/core/index.js.map +1 -1
- package/build/esm/core/types/DataManager.d.ts +5 -0
- package/build/esm/core/types/DataManager.js.map +1 -1
- package/build/esm/core/types/Normalizer.d.ts +29 -0
- package/build/esm/core/types/Normalizer.js +2 -0
- package/build/esm/core/types/Normalizer.js.map +1 -0
- package/build/esm/react/DataManagerProvider.d.ts +7 -0
- package/build/esm/react/DataManagerProvider.js +12 -0
- package/build/esm/react/DataManagerProvider.js.map +1 -0
- package/build/esm/react/__tests__/DataManagerContext.test.js +7 -6
- package/build/esm/react/__tests__/DataManagerContext.test.js.map +1 -1
- package/build/esm/react/__tests__/withDataManager.test.js +3 -0
- package/build/esm/react/__tests__/withDataManager.test.js.map +1 -1
- package/build/esm/react/index.d.ts +2 -0
- package/build/esm/react/index.js +1 -0
- package/build/esm/react/index.js.map +1 -1
- package/build/esm/react-query/ClientDataManager.d.ts +16 -3
- package/build/esm/react-query/ClientDataManager.js +152 -7
- package/build/esm/react-query/ClientDataManager.js.map +1 -1
- package/build/esm/react-query/DataSourceProvider.d.ts +7 -0
- package/build/esm/react-query/DataSourceProvider.js +28 -0
- package/build/esm/react-query/DataSourceProvider.js.map +1 -0
- package/build/esm/react-query/__tests__/createQueryNormalizer.test.js +174 -0
- package/build/esm/react-query/__tests__/createQueryNormalizer.test.js.map +1 -0
- package/build/esm/react-query/__tests__/normalizationEdgeCases.test.js +98 -0
- package/build/esm/react-query/__tests__/normalizationEdgeCases.test.js.map +1 -0
- package/build/esm/react-query/__tests__/subscriptions.test.js +1176 -0
- package/build/esm/react-query/__tests__/subscriptions.test.js.map +1 -0
- package/build/esm/react-query/__tests__/threeLevelIntegration.test.js +656 -0
- package/build/esm/react-query/__tests__/threeLevelIntegration.test.js.map +1 -0
- package/build/esm/react-query/__tests__/updateQueriesFromMutationData.test.js +227 -0
- package/build/esm/react-query/__tests__/updateQueriesFromMutationData.test.js.map +1 -0
- package/build/esm/react-query/hooks/__tests__/useQueryData.refetch.test.js +192 -0
- package/build/esm/react-query/hooks/__tests__/useQueryData.refetch.test.js.map +1 -0
- package/build/esm/react-query/impl/infinite/hooks.js +5 -2
- package/build/esm/react-query/impl/infinite/hooks.js.map +1 -1
- package/build/esm/react-query/impl/infinite/types.d.ts +2 -2
- package/build/esm/react-query/impl/infinite/types.js.map +1 -1
- package/build/esm/react-query/impl/infinite/utils.js +6 -1
- package/build/esm/react-query/impl/infinite/utils.js.map +1 -1
- package/build/esm/react-query/impl/plain/hooks.js +5 -2
- package/build/esm/react-query/impl/plain/hooks.js.map +1 -1
- package/build/esm/react-query/impl/plain/types.d.ts +2 -2
- package/build/esm/react-query/impl/plain/types.js.map +1 -1
- package/build/esm/react-query/impl/plain/utils.js +6 -1
- package/build/esm/react-query/impl/plain/utils.js.map +1 -1
- package/build/esm/react-query/index.d.ts +2 -0
- package/build/esm/react-query/index.js +1 -0
- package/build/esm/react-query/index.js.map +1 -1
- package/build/esm/react-query/types/normalizer.d.ts +21 -0
- package/build/esm/react-query/types/normalizer.js +2 -0
- package/build/esm/react-query/types/normalizer.js.map +1 -0
- package/build/esm/react-query/types/options.d.ts +12 -0
- package/build/esm/react-query/types/options.js.map +1 -1
- package/build/esm/react-query/types/query-meta.d.ts +12 -0
- package/build/esm/react-query/utils/__tests__/checkMutationObjectsKeys.test.js +292 -0
- package/build/esm/react-query/utils/__tests__/checkMutationObjectsKeys.test.js.map +1 -0
- package/build/esm/react-query/utils/checkMutationObjectsKeys.d.ts +17 -0
- package/build/esm/react-query/utils/checkMutationObjectsKeys.js +81 -0
- package/build/esm/react-query/utils/checkMutationObjectsKeys.js.map +1 -0
- package/build/esm/react-query/utils/normalize.d.ts +22 -0
- package/build/esm/react-query/utils/normalize.js +143 -0
- package/build/esm/react-query/utils/normalize.js.map +1 -0
- package/build/esm/react-query/utils/parseQueryKey.d.ts +2 -0
- package/build/esm/react-query/utils/parseQueryKey.js +4 -0
- package/build/esm/react-query/utils/parseQueryKey.js.map +1 -0
- package/build/esm/react-query/utils/warn.d.ts +1 -0
- package/build/esm/react-query/utils/warn.js +9 -0
- package/build/esm/react-query/utils/warn.js.map +1 -0
- package/build/esm/react-query/utils/warnDisabledRefetch.d.ts +1 -0
- package/build/esm/react-query/utils/warnDisabledRefetch.js +5 -0
- package/build/esm/react-query/utils/warnDisabledRefetch.js.map +1 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -10,16 +10,45 @@ npm install @gravity-ui/data-source @tanstack/react-query
|
|
|
10
10
|
|
|
11
11
|
`@tanstack/react-query` is a peer dependency.
|
|
12
12
|
|
|
13
|
-
##
|
|
13
|
+
## Quick Start
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
### 1. Setup DataManager
|
|
16
|
+
|
|
17
|
+
First, create and provide a `DataManager` in your application:
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
import React from 'react';
|
|
21
|
+
import {ClientDataManager, DataManagerContext} from '@gravity-ui/data-source';
|
|
22
|
+
|
|
23
|
+
const dataManager = new ClientDataManager({
|
|
24
|
+
defaultOptions: {
|
|
25
|
+
queries: {
|
|
26
|
+
staleTime: 5 * 60 * 1000, // 5 minutes
|
|
27
|
+
retry: 3,
|
|
28
|
+
},
|
|
29
|
+
// ... other react-query options
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
function App() {
|
|
34
|
+
return (
|
|
35
|
+
<DataManagerContext.Provider value={dataManager}>
|
|
36
|
+
<YourApplication />
|
|
37
|
+
</DataManagerContext.Provider>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### 2. Define Error Types and Wrappers
|
|
43
|
+
|
|
44
|
+
Define a type of error and make your constructors for data sources based on default constructors:
|
|
16
45
|
|
|
17
46
|
```ts
|
|
18
47
|
import {makePlainQueryDataSource as makePlainQueryDataSourceBase} from '@gravity-ui/data-source';
|
|
19
48
|
|
|
20
49
|
export interface ApiError {
|
|
50
|
+
code: number;
|
|
21
51
|
title: string;
|
|
22
|
-
code?: number;
|
|
23
52
|
description?: string;
|
|
24
53
|
}
|
|
25
54
|
|
|
@@ -30,7 +59,9 @@ export const makePlainQueryDataSource = <TParams, TRequest, TResponse, TData, TE
|
|
|
30
59
|
};
|
|
31
60
|
```
|
|
32
61
|
|
|
33
|
-
|
|
62
|
+
### 3. Create Custom DataLoader Component
|
|
63
|
+
|
|
64
|
+
Write a `DataLoader` component based on default to define your display of loading status and errors:
|
|
34
65
|
|
|
35
66
|
```tsx
|
|
36
67
|
import {
|
|
@@ -46,37 +77,743 @@ export interface DataLoaderProps
|
|
|
46
77
|
}
|
|
47
78
|
|
|
48
79
|
export const DataLoader: React.FC<DataLoaderProps> = ({
|
|
49
|
-
LoadingView = YourLoader,
|
|
50
|
-
ErrorView = YourError,
|
|
80
|
+
LoadingView = YourLoader, // You can use your own loader component
|
|
81
|
+
ErrorView = YourError, // You can use your own error component
|
|
51
82
|
...restProps
|
|
52
83
|
}) => {
|
|
53
84
|
return <DataLoaderBase LoadingView={LoadingView} ErrorView={ErrorView} {...restProps} />;
|
|
54
85
|
};
|
|
55
86
|
```
|
|
56
87
|
|
|
57
|
-
Define
|
|
88
|
+
### 4. Define Your First Data Source
|
|
58
89
|
|
|
59
90
|
```ts
|
|
60
|
-
|
|
91
|
+
import {skipContext} from '@gravity-ui/data-source';
|
|
92
|
+
|
|
93
|
+
// Your API function
|
|
94
|
+
import {fetchUser} from './api';
|
|
95
|
+
|
|
96
|
+
export const userDataSource = makePlainQueryDataSource({
|
|
61
97
|
// Keys have to be unique. Maybe you should create a helper for making names of data sources
|
|
62
|
-
name: '
|
|
63
|
-
// skipContext is
|
|
64
|
-
fetch: skipContext(
|
|
98
|
+
name: 'user',
|
|
99
|
+
// skipContext is a helper to skip 2 first parameters in the function (context and fetchContext)
|
|
100
|
+
fetch: skipContext(fetchUser),
|
|
101
|
+
// Optional: generate tags for advanced cache invalidation
|
|
102
|
+
tags: (params) => [`user:${params.userId}`, 'users'],
|
|
65
103
|
});
|
|
66
104
|
```
|
|
67
105
|
|
|
68
|
-
Use
|
|
106
|
+
### 5. Use in Components
|
|
69
107
|
|
|
70
108
|
```tsx
|
|
71
109
|
import {useQueryData} from '@gravity-ui/data-source';
|
|
72
110
|
|
|
73
|
-
export const
|
|
74
|
-
const {data, status, error, refetch} = useQueryData(
|
|
111
|
+
export const UserProfile: React.FC<{userId: number}> = ({userId}) => {
|
|
112
|
+
const {data, status, error, refetch} = useQueryData(userDataSource, {userId});
|
|
75
113
|
|
|
76
114
|
return (
|
|
77
115
|
<DataLoader status={status} error={error} errorAction={refetch}>
|
|
78
|
-
{data && <
|
|
116
|
+
{data && <UserCard user={data} />}
|
|
117
|
+
</DataLoader>
|
|
118
|
+
);
|
|
119
|
+
};
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Core Concepts
|
|
123
|
+
|
|
124
|
+
### Data Source Types
|
|
125
|
+
|
|
126
|
+
The library provides two main types of data sources:
|
|
127
|
+
|
|
128
|
+
#### Plain Query Data Source
|
|
129
|
+
|
|
130
|
+
For simple request/response patterns:
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
const userDataSource = makePlainQueryDataSource({
|
|
134
|
+
name: 'user',
|
|
135
|
+
fetch: skipContext(async (params: {userId: number}) => {
|
|
136
|
+
const response = await fetch(`/api/users/${params.userId}`);
|
|
137
|
+
return response.json();
|
|
138
|
+
}),
|
|
139
|
+
});
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
#### Infinite Query Data Source
|
|
143
|
+
|
|
144
|
+
For pagination and infinite scrolling:
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
const postsDataSource = makeInfiniteQueryDataSource({
|
|
148
|
+
name: 'posts',
|
|
149
|
+
fetch: skipContext(async (params: {page: number; limit: number}) => {
|
|
150
|
+
const response = await fetch(`/api/posts?page=${params.page}&limit=${params.limit}`);
|
|
151
|
+
return response.json();
|
|
152
|
+
}),
|
|
153
|
+
next: (lastPage, allPages) => {
|
|
154
|
+
if (lastPage.hasNext) {
|
|
155
|
+
return {page: allPages.length + 1, limit: 20};
|
|
156
|
+
}
|
|
157
|
+
return undefined;
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Status Management
|
|
163
|
+
|
|
164
|
+
The library normalizes query states into three simple statuses:
|
|
165
|
+
|
|
166
|
+
- `loading` - Actual data loading. The same as `isLoading` in React Query
|
|
167
|
+
- `success` - Data available (may be skipped using idle)
|
|
168
|
+
- `error` - Failed to fetch data
|
|
169
|
+
|
|
170
|
+
### Idle Concept
|
|
171
|
+
|
|
172
|
+
The library provides a special `idle` symbol for skipping query execution:
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
import {idle} from '@gravity-ui/data-source';
|
|
176
|
+
|
|
177
|
+
const UserProfile: React.FC<{userId?: number}> = ({userId}) => {
|
|
178
|
+
// Query won't execute if userId is not defined
|
|
179
|
+
const {data, status} = useQueryData(userDataSource, userId ? {userId} : idle);
|
|
180
|
+
|
|
181
|
+
return (
|
|
182
|
+
<DataLoader status={status} error={null}>
|
|
183
|
+
{data && <UserCard user={data} />}
|
|
79
184
|
</DataLoader>
|
|
80
185
|
);
|
|
81
186
|
};
|
|
82
187
|
```
|
|
188
|
+
|
|
189
|
+
When parameters equal `idle`:
|
|
190
|
+
|
|
191
|
+
- Query doesn't execute
|
|
192
|
+
- Status remains `success`
|
|
193
|
+
- Data remains `undefined`
|
|
194
|
+
- Component can safely render without loading
|
|
195
|
+
|
|
196
|
+
**Benefits of `idle`:**
|
|
197
|
+
|
|
198
|
+
1. **Type Safety** - TypeScript correctly infers types for conditional parameters
|
|
199
|
+
2. **Performance** - Avoids unnecessary server requests
|
|
200
|
+
3. **Logic Simplicity** - No need to manage additional `enabled` state
|
|
201
|
+
4. **Consistency** - Unified approach for all conditional queries
|
|
202
|
+
|
|
203
|
+
This is especially useful for conditional queries when you want to load data only under certain conditions while maintaining type safety.
|
|
204
|
+
|
|
205
|
+
## API Reference
|
|
206
|
+
|
|
207
|
+
### Creating Data Sources
|
|
208
|
+
|
|
209
|
+
#### `makePlainQueryDataSource(config)`
|
|
210
|
+
|
|
211
|
+
Creates a plain query data source for simple request/response patterns.
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
const dataSource = makePlainQueryDataSource({
|
|
215
|
+
name: 'unique-name',
|
|
216
|
+
fetch: skipContext(fetchFunction),
|
|
217
|
+
transformParams: (params) => transformedRequest,
|
|
218
|
+
transformResponse: (response) => transformedData,
|
|
219
|
+
tags: (params) => ['tag1', 'tag2'],
|
|
220
|
+
options: {
|
|
221
|
+
staleTime: 60000,
|
|
222
|
+
retry: 3,
|
|
223
|
+
// ... other react-query options
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
**Parameters:**
|
|
229
|
+
|
|
230
|
+
- `name` - Unique identifier for the data source
|
|
231
|
+
- `fetch` - Function that performs the actual data fetching
|
|
232
|
+
- `transformParams` (optional) - Transform input parameters before request
|
|
233
|
+
- `transformResponse` (optional) - Transform response data
|
|
234
|
+
- `tags` (optional) - Generate cache tags for invalidation
|
|
235
|
+
- `options` (optional) - React Query options
|
|
236
|
+
|
|
237
|
+
#### `makeInfiniteQueryDataSource(config)`
|
|
238
|
+
|
|
239
|
+
Creates an infinite query data source for pagination and infinite scrolling patterns.
|
|
240
|
+
|
|
241
|
+
```ts
|
|
242
|
+
const infiniteDataSource = makeInfiniteQueryDataSource({
|
|
243
|
+
name: 'infinite-data',
|
|
244
|
+
fetch: skipContext(fetchFunction),
|
|
245
|
+
next: (lastPage, allPages) => nextPageParam || undefined,
|
|
246
|
+
prev: (firstPage, allPages) => prevPageParam || undefined,
|
|
247
|
+
// ... other options same as plain
|
|
248
|
+
});
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
**Additional Parameters:**
|
|
252
|
+
|
|
253
|
+
- `next` - Function to determine next page parameters
|
|
254
|
+
- `prev` (optional) - Function to determine previous page parameters
|
|
255
|
+
|
|
256
|
+
### React Hooks
|
|
257
|
+
|
|
258
|
+
#### `useQueryData(dataSource, params, options?)`
|
|
259
|
+
|
|
260
|
+
Main hook for fetching data with a data source.
|
|
261
|
+
|
|
262
|
+
```ts
|
|
263
|
+
const {data, status, error, refetch, ...rest} = useQueryData(
|
|
264
|
+
userDataSource,
|
|
265
|
+
{userId: 123},
|
|
266
|
+
{
|
|
267
|
+
enabled: true,
|
|
268
|
+
refetchInterval: 30000,
|
|
269
|
+
},
|
|
270
|
+
);
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
**Returns:**
|
|
274
|
+
|
|
275
|
+
- `data` - The fetched data
|
|
276
|
+
- `status` - Current status ('loading' | 'success' | 'error')
|
|
277
|
+
- `error` - Error object if request failed
|
|
278
|
+
- `refetch` - Function to manually refetch data
|
|
279
|
+
- Other React Query properties
|
|
280
|
+
|
|
281
|
+
#### `useQueryResponses(responses)`
|
|
282
|
+
|
|
283
|
+
Combines multiple query responses into a single state.
|
|
284
|
+
|
|
285
|
+
```ts
|
|
286
|
+
const user = useQueryData(userDataSource, {userId});
|
|
287
|
+
const posts = useQueryData(postsDataSource, {userId});
|
|
288
|
+
|
|
289
|
+
const {status, error, refetch, refetchErrored} = useQueryResponses([user, posts]);
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
**Returns:**
|
|
293
|
+
|
|
294
|
+
- `status` - Combined status of all queries
|
|
295
|
+
- `error` - First error encountered
|
|
296
|
+
- `refetch` - Function to refetch all queries
|
|
297
|
+
- `refetchErrored` - Function to refetch only failed queries
|
|
298
|
+
|
|
299
|
+
#### `useRefetchAll(states)`
|
|
300
|
+
|
|
301
|
+
Creates a callback to refetch multiple queries.
|
|
302
|
+
|
|
303
|
+
```ts
|
|
304
|
+
const refetchAll = useRefetchAll([user, posts, comments]);
|
|
305
|
+
// refetchAll() will trigger refetch for all queries
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
#### `useRefetchErrored(states)`
|
|
309
|
+
|
|
310
|
+
Creates a callback to refetch only failed queries.
|
|
311
|
+
|
|
312
|
+
```ts
|
|
313
|
+
const refetchErrored = useRefetchErrored([user, posts, comments]);
|
|
314
|
+
// refetchErrored() will only refetch queries with errors
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
#### `useDataManager()`
|
|
318
|
+
|
|
319
|
+
Returns the DataManager from context.
|
|
320
|
+
|
|
321
|
+
```ts
|
|
322
|
+
const dataManager = useDataManager();
|
|
323
|
+
await dataManager.invalidateTag('users');
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
#### `useQueryContext()`
|
|
327
|
+
|
|
328
|
+
Returns the query context (for building custom data hooks base on react-query).
|
|
329
|
+
|
|
330
|
+
### React Components
|
|
331
|
+
|
|
332
|
+
#### `<DataLoader />`
|
|
333
|
+
|
|
334
|
+
Component for handling loading states and errors.
|
|
335
|
+
|
|
336
|
+
```tsx
|
|
337
|
+
<DataLoader
|
|
338
|
+
status={status}
|
|
339
|
+
error={error}
|
|
340
|
+
errorAction={refetch}
|
|
341
|
+
LoadingView={SpinnerComponent}
|
|
342
|
+
ErrorView={ErrorComponent}
|
|
343
|
+
loadingViewProps={{size: 'large'}}
|
|
344
|
+
errorViewProps={{showDetails: true}}
|
|
345
|
+
>
|
|
346
|
+
{data && <YourContent data={data} />}
|
|
347
|
+
</DataLoader>
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
**Props:**
|
|
351
|
+
|
|
352
|
+
- `status` - Current loading status
|
|
353
|
+
- `error` - Error object
|
|
354
|
+
- `errorAction` - Function or action config for error retry
|
|
355
|
+
- `LoadingView` - Component to show during loading
|
|
356
|
+
- `ErrorView` - Component to show on error
|
|
357
|
+
- `loadingViewProps` - Props passed to LoadingView
|
|
358
|
+
- `errorViewProps` - Props passed to ErrorView
|
|
359
|
+
|
|
360
|
+
#### `<DataInfiniteLoader />`
|
|
361
|
+
|
|
362
|
+
Specialized component for infinite queries.
|
|
363
|
+
|
|
364
|
+
```tsx
|
|
365
|
+
<DataInfiniteLoader
|
|
366
|
+
status={status}
|
|
367
|
+
error={error}
|
|
368
|
+
hasNextPage={hasNextPage}
|
|
369
|
+
fetchNextPage={fetchNextPage}
|
|
370
|
+
isFetchingNextPage={isFetchingNextPage}
|
|
371
|
+
LoadingView={SpinnerComponent}
|
|
372
|
+
ErrorView={ErrorComponent}
|
|
373
|
+
MoreView={LoadMoreButton}
|
|
374
|
+
>
|
|
375
|
+
{data.map((item) => (
|
|
376
|
+
<Item key={item.id} data={item} />
|
|
377
|
+
))}
|
|
378
|
+
</DataInfiniteLoader>
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
**Additional Props:**
|
|
382
|
+
|
|
383
|
+
- `hasNextPage` - Whether more pages are available
|
|
384
|
+
- `fetchNextPage` - Function to fetch next page
|
|
385
|
+
- `isFetchingNextPage` - Whether next page is being fetched
|
|
386
|
+
- `MoreView` - Component for "load more" button
|
|
387
|
+
|
|
388
|
+
#### `withDataManager(Component)`
|
|
389
|
+
|
|
390
|
+
HOC that injects DataManager as a prop.
|
|
391
|
+
|
|
392
|
+
```tsx
|
|
393
|
+
const MyComponent = withDataManager<Props>(({dataManager, ...props}) => {
|
|
394
|
+
// Component has access to dataManager
|
|
395
|
+
return <div>...</div>;
|
|
396
|
+
});
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
### Data Management
|
|
400
|
+
|
|
401
|
+
#### `ClientDataManager`
|
|
402
|
+
|
|
403
|
+
Main class for data management.
|
|
404
|
+
|
|
405
|
+
```ts
|
|
406
|
+
const dataManager = new ClientDataManager({
|
|
407
|
+
defaultOptions: {
|
|
408
|
+
queries: {
|
|
409
|
+
staleTime: 300000, // 5 minutes
|
|
410
|
+
retry: 3,
|
|
411
|
+
refetchOnWindowFocus: false,
|
|
412
|
+
},
|
|
413
|
+
},
|
|
414
|
+
});
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
**Methods:**
|
|
418
|
+
|
|
419
|
+
##### `invalidateTag(tag, options?)`
|
|
420
|
+
|
|
421
|
+
Invalidate all queries with a specific tag.
|
|
422
|
+
|
|
423
|
+
```ts
|
|
424
|
+
await dataManager.invalidateTag('users');
|
|
425
|
+
await dataManager.invalidateTag('posts', {
|
|
426
|
+
repeat: {count: 3, interval: 1000}, // Retry invalidation
|
|
427
|
+
});
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
##### `invalidateTags(tags, options?)`
|
|
431
|
+
|
|
432
|
+
Invalidate queries that have all specified tags.
|
|
433
|
+
|
|
434
|
+
```ts
|
|
435
|
+
await dataManager.invalidateTags(['user', 'profile']);
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
##### `invalidateSource(dataSource, options?)`
|
|
439
|
+
|
|
440
|
+
Invalidate all queries for a data source.
|
|
441
|
+
|
|
442
|
+
```ts
|
|
443
|
+
await dataManager.invalidateSource(userDataSource);
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
##### `invalidateParams(dataSource, params, options?)`
|
|
447
|
+
|
|
448
|
+
Invalidate a specific query with exact parameters.
|
|
449
|
+
|
|
450
|
+
```ts
|
|
451
|
+
await dataManager.invalidateParams(userDataSource, {userId: 123});
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
##### `resetSource(dataSource)`
|
|
455
|
+
|
|
456
|
+
Reset (clear) all cached data for a data source.
|
|
457
|
+
|
|
458
|
+
```ts
|
|
459
|
+
await dataManager.resetSource(userDataSource);
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
##### `resetParams(dataSource, params)`
|
|
463
|
+
|
|
464
|
+
Reset cached data for specific parameters.
|
|
465
|
+
|
|
466
|
+
```ts
|
|
467
|
+
await dataManager.resetParams(userDataSource, {userId: 123});
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
##### `invalidateSourceTags(dataSource, params, options?)`
|
|
471
|
+
|
|
472
|
+
Invalidate queries based on tags generated by a data source.
|
|
473
|
+
|
|
474
|
+
```ts
|
|
475
|
+
await dataManager.invalidateSourceTags(userDataSource, {userId: 123});
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
### Utilities
|
|
479
|
+
|
|
480
|
+
#### `skipContext(fetchFunction)`
|
|
481
|
+
|
|
482
|
+
Utility to adapt existing fetch functions to data source interface.
|
|
483
|
+
|
|
484
|
+
```ts
|
|
485
|
+
// Existing function
|
|
486
|
+
async function fetchUser(params: {userId: number}) {
|
|
487
|
+
// ...
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Adapted for data source
|
|
491
|
+
const dataSource = makePlainQueryDataSource({
|
|
492
|
+
name: 'user',
|
|
493
|
+
fetch: skipContext(fetchUser), // Skips context and fetchContext params
|
|
494
|
+
});
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
#### `withCatch(fetchFunction, errorHandler)`
|
|
498
|
+
|
|
499
|
+
Adds standardized error handling to fetch functions.
|
|
500
|
+
|
|
501
|
+
```ts
|
|
502
|
+
const safeFetch = withCatch(fetchUser, (error) => ({error: true, message: error.message}));
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
#### `withCancellation(fetchFunction)`
|
|
506
|
+
|
|
507
|
+
Adds cancellation support to fetch functions.
|
|
508
|
+
|
|
509
|
+
```ts
|
|
510
|
+
const cancellableFetch = withCancellation(fetchFunction);
|
|
511
|
+
// Automatically handles AbortSignal from React Query
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
#### `getProgressiveRefetch(options)`
|
|
515
|
+
|
|
516
|
+
Creates a progressive refetch interval function.
|
|
517
|
+
|
|
518
|
+
```ts
|
|
519
|
+
const progressiveRefetch = getProgressiveRefetch({
|
|
520
|
+
minInterval: 1000, // Start with 1 second
|
|
521
|
+
maxInterval: 30000, // Max 30 seconds
|
|
522
|
+
multiplier: 2, // Double each time
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
const dataSource = makePlainQueryDataSource({
|
|
526
|
+
name: 'data',
|
|
527
|
+
fetch: skipContext(fetchData),
|
|
528
|
+
options: {
|
|
529
|
+
refetchInterval: progressiveRefetch,
|
|
530
|
+
},
|
|
531
|
+
});
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
#### `normalizeStatus(status, fetchStatus)`
|
|
535
|
+
|
|
536
|
+
Converts React Query statuses to DataLoader status.
|
|
537
|
+
|
|
538
|
+
```ts
|
|
539
|
+
const status = normalizeStatus('pending', 'fetching'); // 'loading'
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
#### Status and Error Utilities
|
|
543
|
+
|
|
544
|
+
```ts
|
|
545
|
+
// Get combined status from multiple states
|
|
546
|
+
const status = getStatus([user, posts, comments]);
|
|
547
|
+
|
|
548
|
+
// Get first error from multiple states
|
|
549
|
+
const error = getError([user, posts, comments]);
|
|
550
|
+
|
|
551
|
+
// Merge multiple statuses
|
|
552
|
+
const combinedStatus = mergeStatuses(['loading', 'success', 'error']); // 'error'
|
|
553
|
+
|
|
554
|
+
// Check if query key has a tag
|
|
555
|
+
const hasUserTag = hasTag(queryKey, 'users');
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
#### Key Composition Utilities
|
|
559
|
+
|
|
560
|
+
```ts
|
|
561
|
+
// Compose cache key for a data source
|
|
562
|
+
const key = composeKey(userDataSource, {userId: 123});
|
|
563
|
+
|
|
564
|
+
// Compose full key including tags
|
|
565
|
+
const fullKey = composeFullKey(userDataSource, {userId: 123});
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
#### Constants
|
|
569
|
+
|
|
570
|
+
```ts
|
|
571
|
+
import {idle} from '@gravity-ui/data-source';
|
|
572
|
+
|
|
573
|
+
// Special symbol for skipping query execution
|
|
574
|
+
const params = shouldFetch ? {userId: 123} : idle;
|
|
575
|
+
|
|
576
|
+
// Type-safe alternative to enabled: false
|
|
577
|
+
// Instead of:
|
|
578
|
+
const {data} = useQueryData(userDataSource, {userId: userId || ''}, {enabled: Boolean(userId)});
|
|
579
|
+
|
|
580
|
+
// Use:
|
|
581
|
+
const {data} = useQueryData(userDataSource, userId ? {userId} : idle);
|
|
582
|
+
// TypeScript correctly infers types for both branches
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
#### Query Options Composition
|
|
586
|
+
|
|
587
|
+
```ts
|
|
588
|
+
// Compose React Query options for plain queries
|
|
589
|
+
const plainOptions = composePlainQueryOptions(context, dataSource, params, options);
|
|
590
|
+
|
|
591
|
+
// Compose React Query options for infinite queries
|
|
592
|
+
const infiniteOptions = composeInfiniteQueryOptions(context, dataSource, params, options);
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
**Note:** These functions are primarily for internal use when creating custom data source implementations.
|
|
596
|
+
|
|
597
|
+
## Advanced Patterns
|
|
598
|
+
|
|
599
|
+
### Conditional Queries with Idle
|
|
600
|
+
|
|
601
|
+
Use `idle` to create conditional queries:
|
|
602
|
+
|
|
603
|
+
```ts
|
|
604
|
+
import {idle} from '@gravity-ui/data-source';
|
|
605
|
+
|
|
606
|
+
const ConditionalDataComponent: React.FC<{
|
|
607
|
+
userId?: number;
|
|
608
|
+
shouldLoadPosts: boolean;
|
|
609
|
+
}> = ({userId, shouldLoadPosts}) => {
|
|
610
|
+
// Load user only if userId is defined
|
|
611
|
+
const user = useQueryData(
|
|
612
|
+
userDataSource,
|
|
613
|
+
userId ? {userId} : idle
|
|
614
|
+
);
|
|
615
|
+
|
|
616
|
+
// Load posts only if user is loaded and flag is enabled
|
|
617
|
+
const posts = useQueryData(
|
|
618
|
+
userPostsDataSource,
|
|
619
|
+
user.data && shouldLoadPosts ? {userId: user.data.id} : idle
|
|
620
|
+
);
|
|
621
|
+
|
|
622
|
+
const combined = useQueryResponses([user, posts]);
|
|
623
|
+
|
|
624
|
+
return (
|
|
625
|
+
<DataLoader status={combined.status} error={combined.error}>
|
|
626
|
+
<div>
|
|
627
|
+
{user.data && <UserInfo user={user.data} />}
|
|
628
|
+
{posts.data && <UserPosts posts={posts.data} />}
|
|
629
|
+
</div>
|
|
630
|
+
</DataLoader>
|
|
631
|
+
);
|
|
632
|
+
};
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
### Data Transformation
|
|
636
|
+
|
|
637
|
+
Transform request parameters and response data:
|
|
638
|
+
|
|
639
|
+
```ts
|
|
640
|
+
const apiDataSource = makePlainQueryDataSource({
|
|
641
|
+
name: 'api-data',
|
|
642
|
+
transformParams: (params: {id: number}) => ({
|
|
643
|
+
userId: params.id,
|
|
644
|
+
apiVersion: 'v2',
|
|
645
|
+
format: 'json',
|
|
646
|
+
}),
|
|
647
|
+
transformResponse: (response: ApiResponse) => ({
|
|
648
|
+
user: response.data.user,
|
|
649
|
+
metadata: response.meta,
|
|
650
|
+
}),
|
|
651
|
+
fetch: skipContext(apiFetch),
|
|
652
|
+
});
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
### Tag-Based Cache Invalidation
|
|
656
|
+
|
|
657
|
+
Use tags for sophisticated cache management:
|
|
658
|
+
|
|
659
|
+
```ts
|
|
660
|
+
const userDataSource = makePlainQueryDataSource({
|
|
661
|
+
name: 'user',
|
|
662
|
+
tags: (params) => [`user:${params.userId}`, 'users', 'profiles'],
|
|
663
|
+
fetch: skipContext(fetchUser),
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
const userPostsDataSource = makePlainQueryDataSource({
|
|
667
|
+
name: 'user-posts',
|
|
668
|
+
tags: (params) => [`user:${params.userId}`, 'posts'],
|
|
669
|
+
fetch: skipContext(fetchUserPosts),
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
// Invalidate all data for specific user
|
|
673
|
+
await dataManager.invalidateTag('user:123');
|
|
674
|
+
|
|
675
|
+
// Invalidate all user-related data
|
|
676
|
+
await dataManager.invalidateTag('users');
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
### Error Handling with Types
|
|
680
|
+
|
|
681
|
+
Create type-safe error handling:
|
|
682
|
+
|
|
683
|
+
```ts
|
|
684
|
+
interface ApiError {
|
|
685
|
+
code: number;
|
|
686
|
+
message: string;
|
|
687
|
+
details?: Record<string, unknown>;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
const ErrorView: React.FC<ErrorViewProps<ApiError>> = ({error, action}) => (
|
|
691
|
+
<div className="error">
|
|
692
|
+
<h3>Error {error?.code}</h3>
|
|
693
|
+
<p>{error?.message}</p>
|
|
694
|
+
{action && (
|
|
695
|
+
<button onClick={action.handler}>
|
|
696
|
+
{action.children || 'Retry'}
|
|
697
|
+
</button>
|
|
698
|
+
)}
|
|
699
|
+
</div>
|
|
700
|
+
);
|
|
701
|
+
```
|
|
702
|
+
|
|
703
|
+
### Infinite Queries with Complex Pagination
|
|
704
|
+
|
|
705
|
+
Handle complex pagination scenarios:
|
|
706
|
+
|
|
707
|
+
```ts
|
|
708
|
+
interface PaginationParams {
|
|
709
|
+
cursor?: string;
|
|
710
|
+
limit?: number;
|
|
711
|
+
filters?: Record<string, unknown>;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
interface PaginatedResponse<T> {
|
|
715
|
+
data: T[];
|
|
716
|
+
nextCursor?: string;
|
|
717
|
+
hasMore: boolean;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
const infiniteDataSource = makeInfiniteQueryDataSource({
|
|
721
|
+
name: 'paginated-data',
|
|
722
|
+
fetch: skipContext(async (params: PaginationParams) => {
|
|
723
|
+
const response = await fetch(`/api/data?${new URLSearchParams(params)}`);
|
|
724
|
+
return response.json() as PaginatedResponse<DataItem>;
|
|
725
|
+
}),
|
|
726
|
+
next: (lastPage) => {
|
|
727
|
+
if (lastPage.hasMore && lastPage.nextCursor) {
|
|
728
|
+
return {cursor: lastPage.nextCursor, limit: 20};
|
|
729
|
+
}
|
|
730
|
+
return undefined;
|
|
731
|
+
},
|
|
732
|
+
});
|
|
733
|
+
```
|
|
734
|
+
|
|
735
|
+
### Combining Multiple Data Sources
|
|
736
|
+
|
|
737
|
+
Combine data from multiple sources:
|
|
738
|
+
|
|
739
|
+
```ts
|
|
740
|
+
const UserProfile: React.FC<{userId: number}> = ({userId}) => {
|
|
741
|
+
const user = useQueryData(userDataSource, {userId});
|
|
742
|
+
const posts = useQueryData(userPostsDataSource, {userId});
|
|
743
|
+
const followers = useQueryData(userFollowersDataSource, {userId});
|
|
744
|
+
|
|
745
|
+
const combined = useQueryResponses([user, posts, followers]);
|
|
746
|
+
|
|
747
|
+
return (
|
|
748
|
+
<DataLoader
|
|
749
|
+
status={combined.status}
|
|
750
|
+
error={combined.error}
|
|
751
|
+
errorAction={combined.refetchErrored} // Only retry failed requests
|
|
752
|
+
LoadingView={ProfileSkeleton}
|
|
753
|
+
ErrorView={ProfileError}
|
|
754
|
+
>
|
|
755
|
+
{user && posts && followers && (
|
|
756
|
+
<div>
|
|
757
|
+
<UserInfo user={user.data} />
|
|
758
|
+
<UserPosts posts={posts.data} />
|
|
759
|
+
<UserFollowers followers={followers.data} />
|
|
760
|
+
</div>
|
|
761
|
+
)}
|
|
762
|
+
</DataLoader>
|
|
763
|
+
);
|
|
764
|
+
};
|
|
765
|
+
```
|
|
766
|
+
|
|
767
|
+
## TypeScript Support
|
|
768
|
+
|
|
769
|
+
The library is built with TypeScript-first approach and provides full type inference:
|
|
770
|
+
|
|
771
|
+
```ts
|
|
772
|
+
// Types are automatically inferred
|
|
773
|
+
const userDataSource = makePlainQueryDataSource({
|
|
774
|
+
name: 'user',
|
|
775
|
+
fetch: skipContext(async (params: {userId: number}): Promise<User> => {
|
|
776
|
+
// Return type is inferred as User
|
|
777
|
+
}),
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
// Hook return type is automatically typed
|
|
781
|
+
const {data} = useQueryData(userDataSource, {userId: 123});
|
|
782
|
+
// data is typed as User | undefined
|
|
783
|
+
```
|
|
784
|
+
|
|
785
|
+
### Custom Error Types
|
|
786
|
+
|
|
787
|
+
Define and use custom error types:
|
|
788
|
+
|
|
789
|
+
```ts
|
|
790
|
+
interface ValidationError {
|
|
791
|
+
field: string;
|
|
792
|
+
message: string;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
interface ApiError {
|
|
796
|
+
type: 'network' | 'validation' | 'server';
|
|
797
|
+
message: string;
|
|
798
|
+
validation?: ValidationError[];
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
const typedDataSource = makePlainQueryDataSource<
|
|
802
|
+
{id: number}, // Params type
|
|
803
|
+
{id: number}, // Request type
|
|
804
|
+
ApiResponse, // Response type
|
|
805
|
+
User, // Data type
|
|
806
|
+
ApiError // Error type
|
|
807
|
+
>({
|
|
808
|
+
name: 'typed-user',
|
|
809
|
+
fetch: skipContext(fetchUser),
|
|
810
|
+
});
|
|
811
|
+
```
|
|
812
|
+
|
|
813
|
+
## Contributing
|
|
814
|
+
|
|
815
|
+
Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of conduct and the process for submitting pull requests.
|
|
816
|
+
|
|
817
|
+
## License
|
|
818
|
+
|
|
819
|
+
MIT License. See [LICENSE](LICENSE) file for details.
|