@trackunit/react-graphql-hooks 1.14.54 → 1.15.1-alpha-c496ead6241.0
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/index.cjs.js +190 -165
- package/index.esm.js +193 -167
- package/package.json +4 -4
- package/src/index.d.ts +1 -1
- package/src/paginationQueryUtils.d.ts +6 -13
- package/src/usePaginationQuery.d.ts +12 -5
package/index.cjs.js
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
require('react/jsx-runtime');
|
|
4
4
|
var i18nLibraryTranslation = require('@trackunit/i18n-library-translation');
|
|
5
|
-
var sharedUtils = require('@trackunit/shared-utils');
|
|
6
5
|
var client = require('@apollo/client');
|
|
7
6
|
var esToolkit = require('es-toolkit');
|
|
8
7
|
var react = require('react');
|
|
9
8
|
var reactComponents = require('@trackunit/react-components');
|
|
9
|
+
var sharedUtils = require('@trackunit/shared-utils');
|
|
10
10
|
|
|
11
11
|
var defaultTranslations = {
|
|
12
12
|
|
|
@@ -50,29 +50,6 @@ const setupLibraryTranslations = () => {
|
|
|
50
50
|
i18nLibraryTranslation.registerTranslations(translations);
|
|
51
51
|
};
|
|
52
52
|
|
|
53
|
-
/**
|
|
54
|
-
* Creates a union of two arrays of edges by a key on the node.
|
|
55
|
-
*
|
|
56
|
-
* @template TEdge
|
|
57
|
-
* @param key The key to use to determine uniqueness
|
|
58
|
-
* @param previousEdges The previous array of edges to merge with the new array
|
|
59
|
-
* @param newEdges The new array of edges to merge with the previous array
|
|
60
|
-
* @returns {TEdge[]} A new array with the edges from the previous array and the new array, but only if the item's key is unique.
|
|
61
|
-
*/
|
|
62
|
-
const unionEdgesByNodeKey = (key, previousEdges, newEdges) => {
|
|
63
|
-
const previousIds = previousEdges?.map(edge => edge.node[key]) || [];
|
|
64
|
-
const newIds = newEdges?.map(edge => edge.node[key]) || [];
|
|
65
|
-
const mergedIds = [...previousIds, ...newIds];
|
|
66
|
-
const uniqueIds = [...new Set(mergedIds)];
|
|
67
|
-
return uniqueIds
|
|
68
|
-
.map(id => {
|
|
69
|
-
const previousEdge = previousEdges?.find(edge => edge.node[key] === id);
|
|
70
|
-
const newEdge = newEdges?.find(edge => edge.node[key] === id);
|
|
71
|
-
return newEdge || previousEdge;
|
|
72
|
-
})
|
|
73
|
-
.filter(sharedUtils.truthy);
|
|
74
|
-
};
|
|
75
|
-
|
|
76
53
|
/**
|
|
77
54
|
* A wrapper around Apollo Client's useLazyQuery that provides stable data by default.
|
|
78
55
|
*
|
|
@@ -128,21 +105,38 @@ const useLazyQuery = (document, options) => {
|
|
|
128
105
|
};
|
|
129
106
|
|
|
130
107
|
/**
|
|
131
|
-
*
|
|
132
|
-
*
|
|
108
|
+
* Creates a union of two arrays of edges by a key on the node.
|
|
109
|
+
*
|
|
110
|
+
* @template TEdge
|
|
111
|
+
* @param key The key to use to determine uniqueness
|
|
112
|
+
* @param previousEdges The previous array of edges to merge with the new array
|
|
113
|
+
* @param newEdges The new array of edges to merge with the previous array
|
|
114
|
+
* @returns {TEdge[]} A new array with the edges from the previous array and the new array, but only if the item's key is unique.
|
|
133
115
|
*/
|
|
134
|
-
const
|
|
135
|
-
const
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
116
|
+
const unionEdgesByNodeKey = (key, previousEdges, newEdges) => {
|
|
117
|
+
const previousIds = previousEdges?.map(edge => edge.node[key]) || [];
|
|
118
|
+
const newIds = newEdges?.map(edge => edge.node[key]) || [];
|
|
119
|
+
const mergedIds = [...previousIds, ...newIds];
|
|
120
|
+
const uniqueIds = [...new Set(mergedIds)];
|
|
121
|
+
return uniqueIds
|
|
122
|
+
.map(id => {
|
|
123
|
+
const previousEdge = previousEdges?.find(edge => edge.node[key] === id);
|
|
124
|
+
const newEdge = newEdges?.find(edge => edge.node[key] === id);
|
|
125
|
+
return newEdge || previousEdge;
|
|
126
|
+
})
|
|
127
|
+
.filter(sharedUtils.truthy);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Creates a direction-aware wrapper around `unionEdgesByNodeKey`.
|
|
132
|
+
* For backward fetches, the argument order is swapped so new (earlier) edges
|
|
133
|
+
* come first and previous edges are appended.
|
|
134
|
+
*/
|
|
135
|
+
const createDirectionAwareUnion = (direction) => {
|
|
136
|
+
if (direction === "backward") {
|
|
137
|
+
return (key, previousEdges, newEdges) => unionEdgesByNodeKey(key, newEdges, previousEdges);
|
|
138
|
+
}
|
|
139
|
+
return unionEdgesByNodeKey;
|
|
146
140
|
};
|
|
147
141
|
/**
|
|
148
142
|
* Creates an update query handler for Apollo's fetchMore.
|
|
@@ -163,11 +157,21 @@ const createUpdateQueryHandler = (params) => {
|
|
|
163
157
|
}
|
|
164
158
|
return undefined;
|
|
165
159
|
}
|
|
160
|
+
// Apollo can return undefined fetchMoreResult when the query document
|
|
161
|
+
// changes and triggers a spurious re-execution (e.g. dynamic query
|
|
162
|
+
// rebuilt from visibleColumns). Skip the update and keep previous data.
|
|
163
|
+
if (fetchMoreResult === undefined || fetchMoreResult === null) {
|
|
164
|
+
params.onLoadingComplete();
|
|
165
|
+
return _previousResult;
|
|
166
|
+
}
|
|
166
167
|
// Type assertion required: Apollo's fetchMoreResult has complex internal types
|
|
167
168
|
// that need to be cast to our TData generic for type-safe usage
|
|
168
169
|
const typedResult = fetchMoreResult;
|
|
169
170
|
params.onLastFetchedUpdate(typedResult);
|
|
170
|
-
const
|
|
171
|
+
const options = {
|
|
172
|
+
unionEdgesByNodeKey: createDirectionAwareUnion(params.direction),
|
|
173
|
+
};
|
|
174
|
+
const result = params.onUpdate(params.prev, typedResult, options);
|
|
171
175
|
params.onPageInfoUpdate(result.pageInfo ?? null);
|
|
172
176
|
params.onDataUpdate(result.data);
|
|
173
177
|
params.onLoadingComplete();
|
|
@@ -201,51 +205,27 @@ const createPaginationReducer = () => {
|
|
|
201
205
|
};
|
|
202
206
|
|
|
203
207
|
/**
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
* with the same content.
|
|
208
|
+
* Stabilizes variables via deep equality and manages an AbortController for
|
|
209
|
+
* cancelling in-flight requests when variables change.
|
|
207
210
|
*/
|
|
208
|
-
const
|
|
211
|
+
const useStableVariablesWithAbort = (variables, skip = false) => {
|
|
209
212
|
const [stableVariables, setStableVariables] = react.useState(variables);
|
|
213
|
+
const [abortController, setAbortController] = react.useState(() => new AbortController());
|
|
214
|
+
const [prevVariables, setPrevVariables] = react.useState(variables);
|
|
215
|
+
if (!esToolkit.isEqual(variables, prevVariables)) {
|
|
216
|
+
setPrevVariables(variables);
|
|
217
|
+
setStableVariables(variables);
|
|
218
|
+
if (!skip) {
|
|
219
|
+
abortController.abort();
|
|
220
|
+
setAbortController(new AbortController());
|
|
221
|
+
}
|
|
222
|
+
}
|
|
210
223
|
reactComponents.useWatch({
|
|
211
224
|
value: variables,
|
|
212
225
|
onChange: setStableVariables,
|
|
213
226
|
skip: !Boolean(variables),
|
|
214
227
|
});
|
|
215
|
-
return
|
|
216
|
-
};
|
|
217
|
-
/**
|
|
218
|
-
* Hook to manage AbortController for cancellable requests.
|
|
219
|
-
* Returns an abort signal that can be passed to fetch requests and a function to create new controllers.
|
|
220
|
-
*/
|
|
221
|
-
const useAbortableRequest = (props, setStableVariables) => {
|
|
222
|
-
const [abortController, setAbortController] = react.useState(() => new AbortController());
|
|
223
|
-
const [prevVariables, setPrevVariables] = react.useState(props.variables);
|
|
224
|
-
if (!esToolkit.isEqual(props.variables, prevVariables)) {
|
|
225
|
-
setPrevVariables(props.variables);
|
|
226
|
-
setStableVariables(props.variables);
|
|
227
|
-
if (!props.skip) {
|
|
228
|
-
abortController.abort();
|
|
229
|
-
const newAbortController = new AbortController();
|
|
230
|
-
setAbortController(newAbortController);
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
const abortControllerRef = react.useRef(abortController);
|
|
234
|
-
react.useEffect(() => {
|
|
235
|
-
abortControllerRef.current = abortController;
|
|
236
|
-
}, [abortController]);
|
|
237
|
-
return { abortSignal: abortController.signal };
|
|
238
|
-
};
|
|
239
|
-
/**
|
|
240
|
-
* Hook to sync a value to a ref and return the ref.
|
|
241
|
-
* Useful for avoiding dependency cycles in effects.
|
|
242
|
-
*/
|
|
243
|
-
const useSyncedRef = (value) => {
|
|
244
|
-
const ref = react.useRef(value);
|
|
245
|
-
react.useEffect(() => {
|
|
246
|
-
ref.current = value;
|
|
247
|
-
}, [value]);
|
|
248
|
-
return ref;
|
|
228
|
+
return { stableVariables, abortController };
|
|
249
229
|
};
|
|
250
230
|
/**
|
|
251
231
|
* `usePaginationQuery` fetches data from a GraphQL query with Relay-style cursor pagination.
|
|
@@ -260,7 +240,7 @@ const useSyncedRef = (value) => {
|
|
|
260
240
|
* or event log with infinite scroll via the Table component.
|
|
261
241
|
*
|
|
262
242
|
* ### When not to use
|
|
263
|
-
* Do not use usePaginationQuery for single-entity queries without pagination — use `useQuery` from
|
|
243
|
+
* Do not use usePaginationQuery for single-entity queries without pagination — use `useQuery` from this library directly.
|
|
264
244
|
*
|
|
265
245
|
* @example
|
|
266
246
|
* const {
|
|
@@ -269,8 +249,7 @@ const useSyncedRef = (value) => {
|
|
|
269
249
|
* pagination,
|
|
270
250
|
* lastFetchedData,
|
|
271
251
|
* } = usePaginationQuery(MyAssetsDocument, {
|
|
272
|
-
* updateQuery: (previous, newData) => {
|
|
273
|
-
* // Here you can use the unionEdgesByNodeKey utils to merge your edges together.
|
|
252
|
+
* updateQuery: (previous, newData, { unionEdgesByNodeKey }) => {
|
|
274
253
|
* if (newData?.assets?.edges) {
|
|
275
254
|
* return {
|
|
276
255
|
* data: {
|
|
@@ -289,141 +268,188 @@ const useSyncedRef = (value) => {
|
|
|
289
268
|
* @template TData - The type of the query result data.
|
|
290
269
|
* @template TVariables - The type of the query variables.
|
|
291
270
|
* @param document - The GraphQL query document.
|
|
292
|
-
* @param props - The properties for configuring the query. This includes the `updateQuery` function for merging new and existing data, and options for pagination such as `pageSize`. Also includes other lazy query hook options
|
|
271
|
+
* @param props - The properties for configuring the query. This includes the `updateQuery` function for merging new and existing data, and options for pagination such as `pageSize`. Also includes other lazy query hook options.
|
|
293
272
|
* @returns {PaginationQuery<TData>} The pagination query result containing data, loading state, pagination controls, and lastFetchedData.
|
|
294
273
|
*/
|
|
295
274
|
const usePaginationQuery = (document, props) => {
|
|
296
|
-
|
|
275
|
+
const pageSize = props.pageSize ?? props.variables?.first ?? reactComponents.defaultPageSize;
|
|
276
|
+
const { stableVariables, abortController } = useStableVariablesWithAbort(props.variables, props.skip);
|
|
297
277
|
const [state, dispatch] = react.useReducer(createPaginationReducer(), {
|
|
298
278
|
data: undefined,
|
|
299
279
|
lastFetchedData: undefined,
|
|
300
280
|
resetTrigger: 0,
|
|
301
281
|
});
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
282
|
+
const updateQueryRef = react.useRef(props.updateQuery);
|
|
283
|
+
react.useEffect(() => {
|
|
284
|
+
updateQueryRef.current = props.updateQuery;
|
|
285
|
+
}, [props.updateQuery]);
|
|
286
|
+
const onErrorRef = react.useRef(props.onError);
|
|
287
|
+
react.useEffect(() => {
|
|
288
|
+
onErrorRef.current = props.onError;
|
|
289
|
+
}, [props.onError]);
|
|
290
|
+
const onCompletedRef = react.useRef(props.onCompleted);
|
|
291
|
+
react.useEffect(() => {
|
|
292
|
+
onCompletedRef.current = props.onCompleted;
|
|
293
|
+
}, [props.onCompleted]);
|
|
294
|
+
// Tracks whether data has been successfully fetched at least once. Until then,
|
|
295
|
+
// we must not call reset() because it wipes the initialCursor that
|
|
296
|
+
// useRelayPagination is holding.
|
|
297
|
+
const hasLoadedDataRef = react.useRef(false);
|
|
298
|
+
const onReset = react.useCallback(() => {
|
|
299
|
+
dispatch({ type: "INCREMENT_RESET_TRIGGER" });
|
|
300
|
+
}, []);
|
|
301
|
+
react.useEffect(() => {
|
|
302
|
+
onReset();
|
|
303
|
+
}, [document, onReset]);
|
|
304
|
+
const { table: { setIsLoading, isLoading, setPageInfo, pageInfo, reset, nextPage, previousPage }, variables: { first, after, last, before }, } = reactComponents.useRelayPagination({
|
|
305
|
+
pageSize,
|
|
306
|
+
onReset,
|
|
307
|
+
initialCursor: props.initialCursor,
|
|
308
|
+
});
|
|
312
309
|
const [, { previousData, fetchMore, networkStatus, loading: lazyLoading }] = useLazyQuery(document, {
|
|
313
|
-
...
|
|
310
|
+
...props,
|
|
311
|
+
variables: stableVariables !== undefined
|
|
312
|
+
? {
|
|
313
|
+
...stableVariables,
|
|
314
|
+
first: pageSize,
|
|
315
|
+
...(props.initialCursor !== undefined ? { after: props.initialCursor } : {}),
|
|
316
|
+
}
|
|
317
|
+
: undefined,
|
|
314
318
|
context: {
|
|
315
|
-
...
|
|
319
|
+
...props.context,
|
|
316
320
|
fetchOptions: {
|
|
317
|
-
...
|
|
318
|
-
signal:
|
|
321
|
+
...props.context?.fetchOptions,
|
|
322
|
+
signal: abortController.signal,
|
|
319
323
|
},
|
|
320
324
|
},
|
|
321
|
-
|
|
322
|
-
notifyOnNetworkStatusChange: true, // Needed to update networkStatus
|
|
325
|
+
notifyOnNetworkStatusChange: true,
|
|
323
326
|
onCompleted: completedData => {
|
|
324
327
|
if (networkStatus === client.NetworkStatus.refetch) {
|
|
325
|
-
// trigger reset for refetchQueries for the provided document.
|
|
326
328
|
dispatch({ type: "INCREMENT_RESET_TRIGGER" });
|
|
327
329
|
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
}
|
|
331
|
-
},
|
|
332
|
-
// This is safe since we have no cache
|
|
330
|
+
onCompletedRef.current?.(completedData);
|
|
331
|
+
}, // This is safe since we have no cache
|
|
333
332
|
// and without it it will make a magic extra call to the query check this
|
|
334
333
|
// https://stackoverflow.com/questions/66441463/fetchmore-request-executed-twice-every-time
|
|
335
334
|
nextFetchPolicy: "network-only",
|
|
336
335
|
initialFetchPolicy: "network-only",
|
|
337
336
|
});
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
}, [document, onReset]);
|
|
344
|
-
const { table: { setIsLoading, isLoading, setPageInfo, pageInfo, reset, nextPage, previousPage }, variables: { first, after, last, before }, } = reactComponents.useRelayPagination({
|
|
345
|
-
pageSize: internalProps.pageSize ?? internalProps.variables?.first ?? reactComponents.defaultPageSize,
|
|
346
|
-
onReset,
|
|
347
|
-
});
|
|
348
|
-
const doFetchMore = react.useCallback((variables, prev) => {
|
|
349
|
-
if (internalProps.skip) {
|
|
350
|
-
setIsLoading(false);
|
|
337
|
+
const executeFetch = react.useCallback((variables, prev, direction) => {
|
|
338
|
+
if (props.skip) {
|
|
339
|
+
if (props.initialCursor === undefined) {
|
|
340
|
+
setIsLoading(false);
|
|
341
|
+
}
|
|
351
342
|
return;
|
|
352
343
|
}
|
|
353
344
|
setIsLoading(true);
|
|
354
|
-
|
|
355
|
-
const fetchMoreVariables =
|
|
345
|
+
const signal = abortController.signal;
|
|
346
|
+
const fetchMoreVariables = { ...stableVariables, ...variables };
|
|
356
347
|
fetchMore({
|
|
357
348
|
variables: fetchMoreVariables,
|
|
358
349
|
updateQuery: createUpdateQueryHandler({
|
|
359
|
-
abortSignal,
|
|
350
|
+
abortSignal: signal,
|
|
360
351
|
prev,
|
|
361
|
-
|
|
362
|
-
|
|
352
|
+
direction,
|
|
353
|
+
onUpdate: updateQueryRef.current,
|
|
354
|
+
onDataUpdate: (data) => {
|
|
355
|
+
hasLoadedDataRef.current = true;
|
|
356
|
+
dispatch({ type: "SET_DATA", payload: data });
|
|
357
|
+
},
|
|
363
358
|
onLastFetchedUpdate: (data) => dispatch({ type: "SET_LAST_FETCHED_DATA", payload: data }),
|
|
364
|
-
onPageInfoUpdate:
|
|
359
|
+
onPageInfoUpdate: incoming => {
|
|
360
|
+
if (direction === "initial") {
|
|
361
|
+
setPageInfo(incoming);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
setPageInfo(prevPageInfo => {
|
|
365
|
+
if (direction === "forward") {
|
|
366
|
+
return {
|
|
367
|
+
...prevPageInfo,
|
|
368
|
+
hasNextPage: incoming?.hasNextPage,
|
|
369
|
+
endCursor: incoming?.endCursor,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
return {
|
|
373
|
+
...prevPageInfo,
|
|
374
|
+
hasPreviousPage: incoming?.hasPreviousPage,
|
|
375
|
+
startCursor: incoming?.startCursor,
|
|
376
|
+
};
|
|
377
|
+
});
|
|
378
|
+
},
|
|
365
379
|
onLoadingComplete: () => setIsLoading(false),
|
|
366
380
|
}),
|
|
367
|
-
// It is apparently not possible to use the onError from the useLazyQuery hook so we have to handle it here.
|
|
368
|
-
// However, if you need to pass in your own onError function, you can do so in the props of the hook.
|
|
369
|
-
// But we ignore the error if the request was aborted.
|
|
370
381
|
}).catch(error => {
|
|
371
382
|
setIsLoading(false);
|
|
372
|
-
if (
|
|
383
|
+
if (signal.aborted) {
|
|
373
384
|
return;
|
|
374
385
|
}
|
|
375
|
-
if (
|
|
376
|
-
return
|
|
377
|
-
}
|
|
378
|
-
else {
|
|
379
|
-
throw error;
|
|
386
|
+
if (onErrorRef.current) {
|
|
387
|
+
return onErrorRef.current(error);
|
|
380
388
|
}
|
|
389
|
+
throw error;
|
|
381
390
|
});
|
|
382
|
-
}, [
|
|
391
|
+
}, [props.skip, props.initialCursor, stableVariables, setIsLoading, fetchMore, abortController, setPageInfo]);
|
|
392
|
+
// Single ref for values that effects need without triggering re-runs.
|
|
393
|
+
// Defined after executeFetch so it can be initialized with the real value.
|
|
394
|
+
const latestRef = react.useRef({
|
|
395
|
+
first,
|
|
396
|
+
after,
|
|
397
|
+
last,
|
|
398
|
+
before,
|
|
399
|
+
data: state.data,
|
|
400
|
+
executeFetch,
|
|
401
|
+
});
|
|
402
|
+
// Sync ref after each render — runs before fetch effects (declaration order)
|
|
403
|
+
react.useEffect(() => {
|
|
404
|
+
latestRef.current.first = first;
|
|
405
|
+
latestRef.current.after = after;
|
|
406
|
+
latestRef.current.last = last;
|
|
407
|
+
latestRef.current.before = before;
|
|
408
|
+
latestRef.current.data = state.data;
|
|
409
|
+
latestRef.current.executeFetch = executeFetch;
|
|
410
|
+
});
|
|
411
|
+
// Reset accumulated data when variables change (skip until first load
|
|
412
|
+
// to preserve initialCursor)
|
|
383
413
|
react.useEffect(() => {
|
|
414
|
+
if (!hasLoadedDataRef.current) {
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
384
417
|
dispatch({ type: "RESET" });
|
|
385
418
|
reset();
|
|
386
419
|
}, [stableVariables, reset]);
|
|
387
|
-
// Store pagination values in refs to avoid triggering unnecessary effect re-runs
|
|
388
|
-
const firstRef = useSyncedRef(first);
|
|
389
|
-
const lastRef = useSyncedRef(last);
|
|
390
|
-
const beforeRef = useSyncedRef(before);
|
|
391
|
-
const dataRef = useSyncedRef(state.data);
|
|
392
|
-
const doFetchMoreRef = useSyncedRef(doFetchMore);
|
|
393
420
|
// Fetch initial page when variables or reset trigger changes
|
|
394
421
|
react.useEffect(() => {
|
|
395
422
|
const fetchVariables = {
|
|
396
|
-
first:
|
|
423
|
+
first: latestRef.current.first ?? pageSize,
|
|
397
424
|
last: undefined,
|
|
398
425
|
before: undefined,
|
|
399
|
-
after: undefined,
|
|
426
|
+
after: latestRef.current.after ?? undefined,
|
|
400
427
|
};
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
// Fetch next/previous page when after cursor changes
|
|
428
|
+
latestRef.current.executeFetch(fetchVariables, undefined, "initial");
|
|
429
|
+
}, [stableVariables, state.resetTrigger, pageSize]);
|
|
430
|
+
// Fetch next/previous page when relay cursor changes
|
|
405
431
|
react.useEffect(() => {
|
|
432
|
+
if (!hasLoadedDataRef.current) {
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
406
435
|
if (after !== undefined && after !== null) {
|
|
407
|
-
const
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
last: lastRef.current,
|
|
411
|
-
before: beforeRef.current,
|
|
412
|
-
};
|
|
413
|
-
doFetchMoreRef.current(fetchVariables, dataRef.current);
|
|
436
|
+
const { first: f, last: l, before: b } = latestRef.current;
|
|
437
|
+
latestRef.current.executeFetch({ first: f, after, last: l, before: b }, latestRef.current.data, "forward");
|
|
438
|
+
return;
|
|
414
439
|
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
440
|
+
if (before !== undefined && before !== null) {
|
|
441
|
+
const { first: f, after: a, last: l } = latestRef.current;
|
|
442
|
+
latestRef.current.executeFetch({ first: f, after: a, last: l, before }, latestRef.current.data, "backward");
|
|
443
|
+
}
|
|
444
|
+
}, [after, before]);
|
|
445
|
+
return react.useMemo(() => ({
|
|
446
|
+
data: state.data ?? previousData,
|
|
447
|
+
previousData,
|
|
448
|
+
loading: isLoading || lazyLoading,
|
|
449
|
+
pagination: { setIsLoading, isLoading, setPageInfo, pageInfo, reset, nextPage, previousPage },
|
|
450
|
+
lastFetchedData: state.lastFetchedData,
|
|
451
|
+
reset,
|
|
452
|
+
}), [
|
|
427
453
|
state.data,
|
|
428
454
|
state.lastFetchedData,
|
|
429
455
|
isLoading,
|
|
@@ -500,7 +526,6 @@ const useQuery = (document, options) => {
|
|
|
500
526
|
*/
|
|
501
527
|
setupLibraryTranslations();
|
|
502
528
|
|
|
503
|
-
exports.unionEdgesByNodeKey = unionEdgesByNodeKey;
|
|
504
529
|
exports.useLazyQuery = useLazyQuery;
|
|
505
530
|
exports.usePaginationQuery = usePaginationQuery;
|
|
506
531
|
exports.useQuery = useQuery;
|
package/index.esm.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import 'react/jsx-runtime';
|
|
2
2
|
import { registerTranslations } from '@trackunit/i18n-library-translation';
|
|
3
|
-
import { truthy, objectKeys } from '@trackunit/shared-utils';
|
|
4
3
|
import { useLazyQuery as useLazyQuery$1, NetworkStatus, useQuery as useQuery$1 } from '@apollo/client';
|
|
5
4
|
import { omit, isEqual } from 'es-toolkit';
|
|
6
|
-
import { useMemo, useReducer,
|
|
7
|
-
import {
|
|
5
|
+
import { useMemo, useReducer, useRef, useEffect, useCallback, useState } from 'react';
|
|
6
|
+
import { defaultPageSize, useRelayPagination, useWatch } from '@trackunit/react-components';
|
|
7
|
+
import { truthy, objectKeys } from '@trackunit/shared-utils';
|
|
8
8
|
|
|
9
9
|
var defaultTranslations = {
|
|
10
10
|
|
|
@@ -48,29 +48,6 @@ const setupLibraryTranslations = () => {
|
|
|
48
48
|
registerTranslations(translations);
|
|
49
49
|
};
|
|
50
50
|
|
|
51
|
-
/**
|
|
52
|
-
* Creates a union of two arrays of edges by a key on the node.
|
|
53
|
-
*
|
|
54
|
-
* @template TEdge
|
|
55
|
-
* @param key The key to use to determine uniqueness
|
|
56
|
-
* @param previousEdges The previous array of edges to merge with the new array
|
|
57
|
-
* @param newEdges The new array of edges to merge with the previous array
|
|
58
|
-
* @returns {TEdge[]} A new array with the edges from the previous array and the new array, but only if the item's key is unique.
|
|
59
|
-
*/
|
|
60
|
-
const unionEdgesByNodeKey = (key, previousEdges, newEdges) => {
|
|
61
|
-
const previousIds = previousEdges?.map(edge => edge.node[key]) || [];
|
|
62
|
-
const newIds = newEdges?.map(edge => edge.node[key]) || [];
|
|
63
|
-
const mergedIds = [...previousIds, ...newIds];
|
|
64
|
-
const uniqueIds = [...new Set(mergedIds)];
|
|
65
|
-
return uniqueIds
|
|
66
|
-
.map(id => {
|
|
67
|
-
const previousEdge = previousEdges?.find(edge => edge.node[key] === id);
|
|
68
|
-
const newEdge = newEdges?.find(edge => edge.node[key] === id);
|
|
69
|
-
return newEdge || previousEdge;
|
|
70
|
-
})
|
|
71
|
-
.filter(truthy);
|
|
72
|
-
};
|
|
73
|
-
|
|
74
51
|
/**
|
|
75
52
|
* A wrapper around Apollo Client's useLazyQuery that provides stable data by default.
|
|
76
53
|
*
|
|
@@ -126,21 +103,38 @@ const useLazyQuery = (document, options) => {
|
|
|
126
103
|
};
|
|
127
104
|
|
|
128
105
|
/**
|
|
129
|
-
*
|
|
130
|
-
*
|
|
106
|
+
* Creates a union of two arrays of edges by a key on the node.
|
|
107
|
+
*
|
|
108
|
+
* @template TEdge
|
|
109
|
+
* @param key The key to use to determine uniqueness
|
|
110
|
+
* @param previousEdges The previous array of edges to merge with the new array
|
|
111
|
+
* @param newEdges The new array of edges to merge with the previous array
|
|
112
|
+
* @returns {TEdge[]} A new array with the edges from the previous array and the new array, but only if the item's key is unique.
|
|
131
113
|
*/
|
|
132
|
-
const
|
|
133
|
-
const
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
114
|
+
const unionEdgesByNodeKey = (key, previousEdges, newEdges) => {
|
|
115
|
+
const previousIds = previousEdges?.map(edge => edge.node[key]) || [];
|
|
116
|
+
const newIds = newEdges?.map(edge => edge.node[key]) || [];
|
|
117
|
+
const mergedIds = [...previousIds, ...newIds];
|
|
118
|
+
const uniqueIds = [...new Set(mergedIds)];
|
|
119
|
+
return uniqueIds
|
|
120
|
+
.map(id => {
|
|
121
|
+
const previousEdge = previousEdges?.find(edge => edge.node[key] === id);
|
|
122
|
+
const newEdge = newEdges?.find(edge => edge.node[key] === id);
|
|
123
|
+
return newEdge || previousEdge;
|
|
124
|
+
})
|
|
125
|
+
.filter(truthy);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Creates a direction-aware wrapper around `unionEdgesByNodeKey`.
|
|
130
|
+
* For backward fetches, the argument order is swapped so new (earlier) edges
|
|
131
|
+
* come first and previous edges are appended.
|
|
132
|
+
*/
|
|
133
|
+
const createDirectionAwareUnion = (direction) => {
|
|
134
|
+
if (direction === "backward") {
|
|
135
|
+
return (key, previousEdges, newEdges) => unionEdgesByNodeKey(key, newEdges, previousEdges);
|
|
136
|
+
}
|
|
137
|
+
return unionEdgesByNodeKey;
|
|
144
138
|
};
|
|
145
139
|
/**
|
|
146
140
|
* Creates an update query handler for Apollo's fetchMore.
|
|
@@ -161,11 +155,21 @@ const createUpdateQueryHandler = (params) => {
|
|
|
161
155
|
}
|
|
162
156
|
return undefined;
|
|
163
157
|
}
|
|
158
|
+
// Apollo can return undefined fetchMoreResult when the query document
|
|
159
|
+
// changes and triggers a spurious re-execution (e.g. dynamic query
|
|
160
|
+
// rebuilt from visibleColumns). Skip the update and keep previous data.
|
|
161
|
+
if (fetchMoreResult === undefined || fetchMoreResult === null) {
|
|
162
|
+
params.onLoadingComplete();
|
|
163
|
+
return _previousResult;
|
|
164
|
+
}
|
|
164
165
|
// Type assertion required: Apollo's fetchMoreResult has complex internal types
|
|
165
166
|
// that need to be cast to our TData generic for type-safe usage
|
|
166
167
|
const typedResult = fetchMoreResult;
|
|
167
168
|
params.onLastFetchedUpdate(typedResult);
|
|
168
|
-
const
|
|
169
|
+
const options = {
|
|
170
|
+
unionEdgesByNodeKey: createDirectionAwareUnion(params.direction),
|
|
171
|
+
};
|
|
172
|
+
const result = params.onUpdate(params.prev, typedResult, options);
|
|
169
173
|
params.onPageInfoUpdate(result.pageInfo ?? null);
|
|
170
174
|
params.onDataUpdate(result.data);
|
|
171
175
|
params.onLoadingComplete();
|
|
@@ -199,51 +203,27 @@ const createPaginationReducer = () => {
|
|
|
199
203
|
};
|
|
200
204
|
|
|
201
205
|
/**
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
* with the same content.
|
|
206
|
+
* Stabilizes variables via deep equality and manages an AbortController for
|
|
207
|
+
* cancelling in-flight requests when variables change.
|
|
205
208
|
*/
|
|
206
|
-
const
|
|
209
|
+
const useStableVariablesWithAbort = (variables, skip = false) => {
|
|
207
210
|
const [stableVariables, setStableVariables] = useState(variables);
|
|
211
|
+
const [abortController, setAbortController] = useState(() => new AbortController());
|
|
212
|
+
const [prevVariables, setPrevVariables] = useState(variables);
|
|
213
|
+
if (!isEqual(variables, prevVariables)) {
|
|
214
|
+
setPrevVariables(variables);
|
|
215
|
+
setStableVariables(variables);
|
|
216
|
+
if (!skip) {
|
|
217
|
+
abortController.abort();
|
|
218
|
+
setAbortController(new AbortController());
|
|
219
|
+
}
|
|
220
|
+
}
|
|
208
221
|
useWatch({
|
|
209
222
|
value: variables,
|
|
210
223
|
onChange: setStableVariables,
|
|
211
224
|
skip: !Boolean(variables),
|
|
212
225
|
});
|
|
213
|
-
return
|
|
214
|
-
};
|
|
215
|
-
/**
|
|
216
|
-
* Hook to manage AbortController for cancellable requests.
|
|
217
|
-
* Returns an abort signal that can be passed to fetch requests and a function to create new controllers.
|
|
218
|
-
*/
|
|
219
|
-
const useAbortableRequest = (props, setStableVariables) => {
|
|
220
|
-
const [abortController, setAbortController] = useState(() => new AbortController());
|
|
221
|
-
const [prevVariables, setPrevVariables] = useState(props.variables);
|
|
222
|
-
if (!isEqual(props.variables, prevVariables)) {
|
|
223
|
-
setPrevVariables(props.variables);
|
|
224
|
-
setStableVariables(props.variables);
|
|
225
|
-
if (!props.skip) {
|
|
226
|
-
abortController.abort();
|
|
227
|
-
const newAbortController = new AbortController();
|
|
228
|
-
setAbortController(newAbortController);
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
const abortControllerRef = useRef(abortController);
|
|
232
|
-
useEffect(() => {
|
|
233
|
-
abortControllerRef.current = abortController;
|
|
234
|
-
}, [abortController]);
|
|
235
|
-
return { abortSignal: abortController.signal };
|
|
236
|
-
};
|
|
237
|
-
/**
|
|
238
|
-
* Hook to sync a value to a ref and return the ref.
|
|
239
|
-
* Useful for avoiding dependency cycles in effects.
|
|
240
|
-
*/
|
|
241
|
-
const useSyncedRef = (value) => {
|
|
242
|
-
const ref = useRef(value);
|
|
243
|
-
useEffect(() => {
|
|
244
|
-
ref.current = value;
|
|
245
|
-
}, [value]);
|
|
246
|
-
return ref;
|
|
226
|
+
return { stableVariables, abortController };
|
|
247
227
|
};
|
|
248
228
|
/**
|
|
249
229
|
* `usePaginationQuery` fetches data from a GraphQL query with Relay-style cursor pagination.
|
|
@@ -258,7 +238,7 @@ const useSyncedRef = (value) => {
|
|
|
258
238
|
* or event log with infinite scroll via the Table component.
|
|
259
239
|
*
|
|
260
240
|
* ### When not to use
|
|
261
|
-
* Do not use usePaginationQuery for single-entity queries without pagination — use `useQuery` from
|
|
241
|
+
* Do not use usePaginationQuery for single-entity queries without pagination — use `useQuery` from this library directly.
|
|
262
242
|
*
|
|
263
243
|
* @example
|
|
264
244
|
* const {
|
|
@@ -267,8 +247,7 @@ const useSyncedRef = (value) => {
|
|
|
267
247
|
* pagination,
|
|
268
248
|
* lastFetchedData,
|
|
269
249
|
* } = usePaginationQuery(MyAssetsDocument, {
|
|
270
|
-
* updateQuery: (previous, newData) => {
|
|
271
|
-
* // Here you can use the unionEdgesByNodeKey utils to merge your edges together.
|
|
250
|
+
* updateQuery: (previous, newData, { unionEdgesByNodeKey }) => {
|
|
272
251
|
* if (newData?.assets?.edges) {
|
|
273
252
|
* return {
|
|
274
253
|
* data: {
|
|
@@ -287,141 +266,188 @@ const useSyncedRef = (value) => {
|
|
|
287
266
|
* @template TData - The type of the query result data.
|
|
288
267
|
* @template TVariables - The type of the query variables.
|
|
289
268
|
* @param document - The GraphQL query document.
|
|
290
|
-
* @param props - The properties for configuring the query. This includes the `updateQuery` function for merging new and existing data, and options for pagination such as `pageSize`. Also includes other lazy query hook options
|
|
269
|
+
* @param props - The properties for configuring the query. This includes the `updateQuery` function for merging new and existing data, and options for pagination such as `pageSize`. Also includes other lazy query hook options.
|
|
291
270
|
* @returns {PaginationQuery<TData>} The pagination query result containing data, loading state, pagination controls, and lastFetchedData.
|
|
292
271
|
*/
|
|
293
272
|
const usePaginationQuery = (document, props) => {
|
|
294
|
-
|
|
273
|
+
const pageSize = props.pageSize ?? props.variables?.first ?? defaultPageSize;
|
|
274
|
+
const { stableVariables, abortController } = useStableVariablesWithAbort(props.variables, props.skip);
|
|
295
275
|
const [state, dispatch] = useReducer(createPaginationReducer(), {
|
|
296
276
|
data: undefined,
|
|
297
277
|
lastFetchedData: undefined,
|
|
298
278
|
resetTrigger: 0,
|
|
299
279
|
});
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
280
|
+
const updateQueryRef = useRef(props.updateQuery);
|
|
281
|
+
useEffect(() => {
|
|
282
|
+
updateQueryRef.current = props.updateQuery;
|
|
283
|
+
}, [props.updateQuery]);
|
|
284
|
+
const onErrorRef = useRef(props.onError);
|
|
285
|
+
useEffect(() => {
|
|
286
|
+
onErrorRef.current = props.onError;
|
|
287
|
+
}, [props.onError]);
|
|
288
|
+
const onCompletedRef = useRef(props.onCompleted);
|
|
289
|
+
useEffect(() => {
|
|
290
|
+
onCompletedRef.current = props.onCompleted;
|
|
291
|
+
}, [props.onCompleted]);
|
|
292
|
+
// Tracks whether data has been successfully fetched at least once. Until then,
|
|
293
|
+
// we must not call reset() because it wipes the initialCursor that
|
|
294
|
+
// useRelayPagination is holding.
|
|
295
|
+
const hasLoadedDataRef = useRef(false);
|
|
296
|
+
const onReset = useCallback(() => {
|
|
297
|
+
dispatch({ type: "INCREMENT_RESET_TRIGGER" });
|
|
298
|
+
}, []);
|
|
299
|
+
useEffect(() => {
|
|
300
|
+
onReset();
|
|
301
|
+
}, [document, onReset]);
|
|
302
|
+
const { table: { setIsLoading, isLoading, setPageInfo, pageInfo, reset, nextPage, previousPage }, variables: { first, after, last, before }, } = useRelayPagination({
|
|
303
|
+
pageSize,
|
|
304
|
+
onReset,
|
|
305
|
+
initialCursor: props.initialCursor,
|
|
306
|
+
});
|
|
310
307
|
const [, { previousData, fetchMore, networkStatus, loading: lazyLoading }] = useLazyQuery(document, {
|
|
311
|
-
...
|
|
308
|
+
...props,
|
|
309
|
+
variables: stableVariables !== undefined
|
|
310
|
+
? {
|
|
311
|
+
...stableVariables,
|
|
312
|
+
first: pageSize,
|
|
313
|
+
...(props.initialCursor !== undefined ? { after: props.initialCursor } : {}),
|
|
314
|
+
}
|
|
315
|
+
: undefined,
|
|
312
316
|
context: {
|
|
313
|
-
...
|
|
317
|
+
...props.context,
|
|
314
318
|
fetchOptions: {
|
|
315
|
-
...
|
|
316
|
-
signal:
|
|
319
|
+
...props.context?.fetchOptions,
|
|
320
|
+
signal: abortController.signal,
|
|
317
321
|
},
|
|
318
322
|
},
|
|
319
|
-
|
|
320
|
-
notifyOnNetworkStatusChange: true, // Needed to update networkStatus
|
|
323
|
+
notifyOnNetworkStatusChange: true,
|
|
321
324
|
onCompleted: completedData => {
|
|
322
325
|
if (networkStatus === NetworkStatus.refetch) {
|
|
323
|
-
// trigger reset for refetchQueries for the provided document.
|
|
324
326
|
dispatch({ type: "INCREMENT_RESET_TRIGGER" });
|
|
325
327
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
}
|
|
329
|
-
},
|
|
330
|
-
// This is safe since we have no cache
|
|
328
|
+
onCompletedRef.current?.(completedData);
|
|
329
|
+
}, // This is safe since we have no cache
|
|
331
330
|
// and without it it will make a magic extra call to the query check this
|
|
332
331
|
// https://stackoverflow.com/questions/66441463/fetchmore-request-executed-twice-every-time
|
|
333
332
|
nextFetchPolicy: "network-only",
|
|
334
333
|
initialFetchPolicy: "network-only",
|
|
335
334
|
});
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
}, [document, onReset]);
|
|
342
|
-
const { table: { setIsLoading, isLoading, setPageInfo, pageInfo, reset, nextPage, previousPage }, variables: { first, after, last, before }, } = useRelayPagination({
|
|
343
|
-
pageSize: internalProps.pageSize ?? internalProps.variables?.first ?? defaultPageSize,
|
|
344
|
-
onReset,
|
|
345
|
-
});
|
|
346
|
-
const doFetchMore = useCallback((variables, prev) => {
|
|
347
|
-
if (internalProps.skip) {
|
|
348
|
-
setIsLoading(false);
|
|
335
|
+
const executeFetch = useCallback((variables, prev, direction) => {
|
|
336
|
+
if (props.skip) {
|
|
337
|
+
if (props.initialCursor === undefined) {
|
|
338
|
+
setIsLoading(false);
|
|
339
|
+
}
|
|
349
340
|
return;
|
|
350
341
|
}
|
|
351
342
|
setIsLoading(true);
|
|
352
|
-
|
|
353
|
-
const fetchMoreVariables =
|
|
343
|
+
const signal = abortController.signal;
|
|
344
|
+
const fetchMoreVariables = { ...stableVariables, ...variables };
|
|
354
345
|
fetchMore({
|
|
355
346
|
variables: fetchMoreVariables,
|
|
356
347
|
updateQuery: createUpdateQueryHandler({
|
|
357
|
-
abortSignal,
|
|
348
|
+
abortSignal: signal,
|
|
358
349
|
prev,
|
|
359
|
-
|
|
360
|
-
|
|
350
|
+
direction,
|
|
351
|
+
onUpdate: updateQueryRef.current,
|
|
352
|
+
onDataUpdate: (data) => {
|
|
353
|
+
hasLoadedDataRef.current = true;
|
|
354
|
+
dispatch({ type: "SET_DATA", payload: data });
|
|
355
|
+
},
|
|
361
356
|
onLastFetchedUpdate: (data) => dispatch({ type: "SET_LAST_FETCHED_DATA", payload: data }),
|
|
362
|
-
onPageInfoUpdate:
|
|
357
|
+
onPageInfoUpdate: incoming => {
|
|
358
|
+
if (direction === "initial") {
|
|
359
|
+
setPageInfo(incoming);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
setPageInfo(prevPageInfo => {
|
|
363
|
+
if (direction === "forward") {
|
|
364
|
+
return {
|
|
365
|
+
...prevPageInfo,
|
|
366
|
+
hasNextPage: incoming?.hasNextPage,
|
|
367
|
+
endCursor: incoming?.endCursor,
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
return {
|
|
371
|
+
...prevPageInfo,
|
|
372
|
+
hasPreviousPage: incoming?.hasPreviousPage,
|
|
373
|
+
startCursor: incoming?.startCursor,
|
|
374
|
+
};
|
|
375
|
+
});
|
|
376
|
+
},
|
|
363
377
|
onLoadingComplete: () => setIsLoading(false),
|
|
364
378
|
}),
|
|
365
|
-
// It is apparently not possible to use the onError from the useLazyQuery hook so we have to handle it here.
|
|
366
|
-
// However, if you need to pass in your own onError function, you can do so in the props of the hook.
|
|
367
|
-
// But we ignore the error if the request was aborted.
|
|
368
379
|
}).catch(error => {
|
|
369
380
|
setIsLoading(false);
|
|
370
|
-
if (
|
|
381
|
+
if (signal.aborted) {
|
|
371
382
|
return;
|
|
372
383
|
}
|
|
373
|
-
if (
|
|
374
|
-
return
|
|
375
|
-
}
|
|
376
|
-
else {
|
|
377
|
-
throw error;
|
|
384
|
+
if (onErrorRef.current) {
|
|
385
|
+
return onErrorRef.current(error);
|
|
378
386
|
}
|
|
387
|
+
throw error;
|
|
379
388
|
});
|
|
380
|
-
}, [
|
|
389
|
+
}, [props.skip, props.initialCursor, stableVariables, setIsLoading, fetchMore, abortController, setPageInfo]);
|
|
390
|
+
// Single ref for values that effects need without triggering re-runs.
|
|
391
|
+
// Defined after executeFetch so it can be initialized with the real value.
|
|
392
|
+
const latestRef = useRef({
|
|
393
|
+
first,
|
|
394
|
+
after,
|
|
395
|
+
last,
|
|
396
|
+
before,
|
|
397
|
+
data: state.data,
|
|
398
|
+
executeFetch,
|
|
399
|
+
});
|
|
400
|
+
// Sync ref after each render — runs before fetch effects (declaration order)
|
|
401
|
+
useEffect(() => {
|
|
402
|
+
latestRef.current.first = first;
|
|
403
|
+
latestRef.current.after = after;
|
|
404
|
+
latestRef.current.last = last;
|
|
405
|
+
latestRef.current.before = before;
|
|
406
|
+
latestRef.current.data = state.data;
|
|
407
|
+
latestRef.current.executeFetch = executeFetch;
|
|
408
|
+
});
|
|
409
|
+
// Reset accumulated data when variables change (skip until first load
|
|
410
|
+
// to preserve initialCursor)
|
|
381
411
|
useEffect(() => {
|
|
412
|
+
if (!hasLoadedDataRef.current) {
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
382
415
|
dispatch({ type: "RESET" });
|
|
383
416
|
reset();
|
|
384
417
|
}, [stableVariables, reset]);
|
|
385
|
-
// Store pagination values in refs to avoid triggering unnecessary effect re-runs
|
|
386
|
-
const firstRef = useSyncedRef(first);
|
|
387
|
-
const lastRef = useSyncedRef(last);
|
|
388
|
-
const beforeRef = useSyncedRef(before);
|
|
389
|
-
const dataRef = useSyncedRef(state.data);
|
|
390
|
-
const doFetchMoreRef = useSyncedRef(doFetchMore);
|
|
391
418
|
// Fetch initial page when variables or reset trigger changes
|
|
392
419
|
useEffect(() => {
|
|
393
420
|
const fetchVariables = {
|
|
394
|
-
first:
|
|
421
|
+
first: latestRef.current.first ?? pageSize,
|
|
395
422
|
last: undefined,
|
|
396
423
|
before: undefined,
|
|
397
|
-
after: undefined,
|
|
424
|
+
after: latestRef.current.after ?? undefined,
|
|
398
425
|
};
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
// Fetch next/previous page when after cursor changes
|
|
426
|
+
latestRef.current.executeFetch(fetchVariables, undefined, "initial");
|
|
427
|
+
}, [stableVariables, state.resetTrigger, pageSize]);
|
|
428
|
+
// Fetch next/previous page when relay cursor changes
|
|
403
429
|
useEffect(() => {
|
|
430
|
+
if (!hasLoadedDataRef.current) {
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
404
433
|
if (after !== undefined && after !== null) {
|
|
405
|
-
const
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
last: lastRef.current,
|
|
409
|
-
before: beforeRef.current,
|
|
410
|
-
};
|
|
411
|
-
doFetchMoreRef.current(fetchVariables, dataRef.current);
|
|
434
|
+
const { first: f, last: l, before: b } = latestRef.current;
|
|
435
|
+
latestRef.current.executeFetch({ first: f, after, last: l, before: b }, latestRef.current.data, "forward");
|
|
436
|
+
return;
|
|
412
437
|
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
438
|
+
if (before !== undefined && before !== null) {
|
|
439
|
+
const { first: f, after: a, last: l } = latestRef.current;
|
|
440
|
+
latestRef.current.executeFetch({ first: f, after: a, last: l, before }, latestRef.current.data, "backward");
|
|
441
|
+
}
|
|
442
|
+
}, [after, before]);
|
|
443
|
+
return useMemo(() => ({
|
|
444
|
+
data: state.data ?? previousData,
|
|
445
|
+
previousData,
|
|
446
|
+
loading: isLoading || lazyLoading,
|
|
447
|
+
pagination: { setIsLoading, isLoading, setPageInfo, pageInfo, reset, nextPage, previousPage },
|
|
448
|
+
lastFetchedData: state.lastFetchedData,
|
|
449
|
+
reset,
|
|
450
|
+
}), [
|
|
425
451
|
state.data,
|
|
426
452
|
state.lastFetchedData,
|
|
427
453
|
isLoading,
|
|
@@ -498,4 +524,4 @@ const useQuery = (document, options) => {
|
|
|
498
524
|
*/
|
|
499
525
|
setupLibraryTranslations();
|
|
500
526
|
|
|
501
|
-
export {
|
|
527
|
+
export { useLazyQuery, usePaginationQuery, useQuery };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trackunit/react-graphql-hooks",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.15.1-alpha-c496ead6241.0",
|
|
4
4
|
"repository": "https://github.com/Trackunit/manager",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.txt",
|
|
6
6
|
"engines": {
|
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
"dependencies": {
|
|
10
10
|
"@apollo/client": "3.13.8",
|
|
11
11
|
"react": "19.0.0",
|
|
12
|
-
"@trackunit/i18n-library-translation": "1.12.
|
|
13
|
-
"@trackunit/shared-utils": "1.13.
|
|
12
|
+
"@trackunit/i18n-library-translation": "1.12.53-alpha-c496ead6241.0",
|
|
13
|
+
"@trackunit/shared-utils": "1.13.62-alpha-c496ead6241.0",
|
|
14
14
|
"es-toolkit": "^1.39.10",
|
|
15
|
-
"@trackunit/react-components": "1.
|
|
15
|
+
"@trackunit/react-components": "1.18.1-alpha-c496ead6241.0"
|
|
16
16
|
},
|
|
17
17
|
"module": "./index.esm.js",
|
|
18
18
|
"main": "./index.cjs.js",
|
package/src/index.d.ts
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import { OperationVariables as ApolloOperationVariables } from "@apollo/client/core/types";
|
|
2
1
|
import { RelayPageInfo } from "@trackunit/react-components";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
after?: string | null;
|
|
2
|
+
import { unionEdgesByNodeKey } from "./unionEdgesByNodeKey";
|
|
3
|
+
export type FetchDirection = "forward" | "backward" | "initial";
|
|
4
|
+
export type UpdateQueryOptions = {
|
|
5
|
+
readonly unionEdgesByNodeKey: typeof unionEdgesByNodeKey;
|
|
8
6
|
};
|
|
9
7
|
export type PaginationState<TData> = {
|
|
10
8
|
data: TData | undefined;
|
|
@@ -22,11 +20,6 @@ export type PaginationAction<TData> = {
|
|
|
22
20
|
} | {
|
|
23
21
|
type: "INCREMENT_RESET_TRIGGER";
|
|
24
22
|
};
|
|
25
|
-
/**
|
|
26
|
-
* Converts pagination variables from cursor-based (first/after) to page-based (page/pageSize) if needed.
|
|
27
|
-
* Returns the appropriate variables based on the pagination type.
|
|
28
|
-
*/
|
|
29
|
-
export declare const convertPaginationVariables: <TVariables extends ApolloOperationVariables>(baseVariables: TVariables | undefined, paginationVars: RelayPaginationQueryVariables) => TVariables;
|
|
30
23
|
/**
|
|
31
24
|
* Creates an update query handler for Apollo's fetchMore.
|
|
32
25
|
* Handles aborted requests and merges new data with existing data.
|
|
@@ -34,7 +27,8 @@ export declare const convertPaginationVariables: <TVariables extends ApolloOpera
|
|
|
34
27
|
export declare const createUpdateQueryHandler: <TData, TPreviousResult>(params: {
|
|
35
28
|
abortSignal: AbortSignal;
|
|
36
29
|
prev: TData | undefined;
|
|
37
|
-
|
|
30
|
+
direction: FetchDirection;
|
|
31
|
+
onUpdate: (prev: TData | undefined, newData: TData, options: UpdateQueryOptions) => {
|
|
38
32
|
data: TData;
|
|
39
33
|
pageInfo?: RelayPageInfo | null;
|
|
40
34
|
};
|
|
@@ -50,4 +44,3 @@ export declare const createUpdateQueryHandler: <TData, TPreviousResult>(params:
|
|
|
50
44
|
* Handles data updates, reset, and reset trigger increments.
|
|
51
45
|
*/
|
|
52
46
|
export declare const createPaginationReducer: <TData>() => (state: PaginationState<TData>, action: PaginationAction<TData>) => PaginationState<TData>;
|
|
53
|
-
export {};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { LazyQueryHookOptions as ApolloLazyQueryHookOptions, OperationVariables as ApolloOperationVariables, TypedDocumentNode as ApolloTypedDocumentNode } from "@apollo/client";
|
|
2
2
|
import { RelayPageInfo, RelayTableSupport } from "@trackunit/react-components";
|
|
3
|
+
import { type UpdateQueryOptions } from "./paginationQueryUtils";
|
|
3
4
|
/**
|
|
4
5
|
* This type is used to return the data from the query, the pagination object and the last fetched data.
|
|
5
6
|
*/
|
|
@@ -18,9 +19,11 @@ export interface PaginationQueryProps<TData, TVariables extends ApolloOperationV
|
|
|
18
19
|
*
|
|
19
20
|
* @param previous The previous data from the query, which can be undefined if no previous data exists.
|
|
20
21
|
* @param newData The new data to merge with the previous data.
|
|
22
|
+
* @param options Contains `unionEdgesByNodeKey` which is direction-aware — for backward
|
|
23
|
+
* fetches it automatically swaps argument order so new (earlier) edges come first.
|
|
21
24
|
* @returns an object containing the merged data, with an optional pageInfo field of type RelayPageInfo or null.
|
|
22
25
|
*/
|
|
23
|
-
updateQuery: (previous: TData | undefined, newData: TData) => {
|
|
26
|
+
updateQuery: (previous: TData | undefined, newData: TData, options: UpdateQueryOptions) => {
|
|
24
27
|
data: TData;
|
|
25
28
|
pageInfo?: RelayPageInfo | null;
|
|
26
29
|
};
|
|
@@ -34,6 +37,11 @@ export interface PaginationQueryProps<TData, TVariables extends ApolloOperationV
|
|
|
34
37
|
* A boolean value that specifies whether to skip the query.
|
|
35
38
|
*/
|
|
36
39
|
skip?: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* When provided, pagination starts from this cursor (middle of the dataset)
|
|
42
|
+
* instead of from the beginning. Useful for resuming scroll position from a URL.
|
|
43
|
+
*/
|
|
44
|
+
initialCursor?: string;
|
|
37
45
|
}
|
|
38
46
|
/**
|
|
39
47
|
* `usePaginationQuery` fetches data from a GraphQL query with Relay-style cursor pagination.
|
|
@@ -48,7 +56,7 @@ export interface PaginationQueryProps<TData, TVariables extends ApolloOperationV
|
|
|
48
56
|
* or event log with infinite scroll via the Table component.
|
|
49
57
|
*
|
|
50
58
|
* ### When not to use
|
|
51
|
-
* Do not use usePaginationQuery for single-entity queries without pagination — use `useQuery` from
|
|
59
|
+
* Do not use usePaginationQuery for single-entity queries without pagination — use `useQuery` from this library directly.
|
|
52
60
|
*
|
|
53
61
|
* @example
|
|
54
62
|
* const {
|
|
@@ -57,8 +65,7 @@ export interface PaginationQueryProps<TData, TVariables extends ApolloOperationV
|
|
|
57
65
|
* pagination,
|
|
58
66
|
* lastFetchedData,
|
|
59
67
|
* } = usePaginationQuery(MyAssetsDocument, {
|
|
60
|
-
* updateQuery: (previous, newData) => {
|
|
61
|
-
* // Here you can use the unionEdgesByNodeKey utils to merge your edges together.
|
|
68
|
+
* updateQuery: (previous, newData, { unionEdgesByNodeKey }) => {
|
|
62
69
|
* if (newData?.assets?.edges) {
|
|
63
70
|
* return {
|
|
64
71
|
* data: {
|
|
@@ -77,7 +84,7 @@ export interface PaginationQueryProps<TData, TVariables extends ApolloOperationV
|
|
|
77
84
|
* @template TData - The type of the query result data.
|
|
78
85
|
* @template TVariables - The type of the query variables.
|
|
79
86
|
* @param document - The GraphQL query document.
|
|
80
|
-
* @param props - The properties for configuring the query. This includes the `updateQuery` function for merging new and existing data, and options for pagination such as `pageSize`. Also includes other lazy query hook options
|
|
87
|
+
* @param props - The properties for configuring the query. This includes the `updateQuery` function for merging new and existing data, and options for pagination such as `pageSize`. Also includes other lazy query hook options.
|
|
81
88
|
* @returns {PaginationQuery<TData>} The pagination query result containing data, loading state, pagination controls, and lastFetchedData.
|
|
82
89
|
*/
|
|
83
90
|
export declare const usePaginationQuery: <TData, TVariables extends ApolloOperationVariables>(document: TypedDocumentNode<TData, TVariables>, props: PaginationQueryProps<TData, TVariables>) => PaginationQuery<TData>;
|