@trackunit/react-graphql-hooks 1.14.53 → 1.15.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 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
- * Converts pagination variables from cursor-based (first/after) to page-based (page/pageSize) if needed.
132
- * Returns the appropriate variables based on the pagination type.
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 convertPaginationVariables = (baseVariables, paginationVars) => {
135
- const usesPageBasedPagination = baseVariables !== undefined && "page" in baseVariables && "pageSize" in baseVariables;
136
- const paginationVariables = usesPageBasedPagination
137
- ? {
138
- pageSize: paginationVars.first,
139
- page: Number(paginationVars.after ?? 0),
140
- }
141
- : { ...paginationVars };
142
- return {
143
- ...baseVariables,
144
- ...paginationVariables,
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 result = params.onUpdate(params.prev, typedResult);
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
- * Hook to stabilize variables based on deep equality comparison.
205
- * Prevents unnecessary re-fetches when parent components pass new variable objects
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 useStableVariables = (variables) => {
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 [stableVariables, setStableVariables];
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 Apollo Client directly.
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 from Apollo Client.
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
- // Use reducer to manage interconnected pagination state
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
- // Stabilize variables to prevent unnecessary re-fetches
303
- const [stableVariables, setStableVariables] = useStableVariables(props.variables);
304
- // Manage abort controller for cancellable requests
305
- const { abortSignal } = useAbortableRequest(props, setStableVariables);
306
- const internalProps = react.useMemo(() => {
307
- return {
308
- ...props,
309
- variables: stableVariables,
310
- };
311
- }, [props, stableVariables]);
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
- ...internalProps,
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
- ...internalProps.context,
319
+ ...props.context,
316
320
  fetchOptions: {
317
- ...internalProps.context?.fetchOptions,
318
- signal: abortSignal,
321
+ ...props.context?.fetchOptions,
322
+ signal: abortController.signal,
319
323
  },
320
324
  },
321
- pollInterval: internalProps.pollInterval,
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
- if (internalProps.onCompleted) {
329
- internalProps.onCompleted(completedData);
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 onReset = react.useCallback(() => {
339
- dispatch({ type: "INCREMENT_RESET_TRIGGER" });
340
- }, []);
341
- react.useEffect(() => {
342
- onReset();
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
- // Convert pagination variables based on pagination type (cursor vs page-based)
355
- const fetchMoreVariables = convertPaginationVariables(internalProps.variables, variables);
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
- onUpdate: internalProps.updateQuery,
362
- onDataUpdate: (data) => dispatch({ type: "SET_DATA", payload: data }),
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: setPageInfo,
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 (abortSignal.aborted) {
383
+ if (signal.aborted) {
373
384
  return;
374
385
  }
375
- if (internalProps.onError) {
376
- return internalProps.onError(error);
377
- }
378
- else {
379
- throw error;
386
+ if (onErrorRef.current) {
387
+ return onErrorRef.current(error);
380
388
  }
389
+ throw error;
381
390
  });
382
- }, [internalProps, setIsLoading, fetchMore, abortSignal, setPageInfo]);
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: firstRef.current,
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
- doFetchMoreRef.current(fetchVariables, undefined);
402
- // Refs are stable and don't need to trigger re-runs, but included to satisfy linter
403
- }, [stableVariables, state.resetTrigger, firstRef, doFetchMoreRef]);
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 fetchVariables = {
408
- first: firstRef.current,
409
- after,
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
- // Refs are stable and don't need to trigger re-runs, but included to satisfy linter
416
- }, [after, firstRef, lastRef, beforeRef, dataRef, doFetchMoreRef]);
417
- return react.useMemo(() => {
418
- return {
419
- data: state.data ?? previousData,
420
- previousData,
421
- loading: isLoading || lazyLoading,
422
- pagination: { setIsLoading, isLoading, setPageInfo, pageInfo, reset, nextPage, previousPage },
423
- lastFetchedData: state.lastFetchedData,
424
- reset,
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, useCallback, useEffect, useState, useRef } from 'react';
7
- import { useRelayPagination, defaultPageSize, useWatch } from '@trackunit/react-components';
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
- * Converts pagination variables from cursor-based (first/after) to page-based (page/pageSize) if needed.
130
- * Returns the appropriate variables based on the pagination type.
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 convertPaginationVariables = (baseVariables, paginationVars) => {
133
- const usesPageBasedPagination = baseVariables !== undefined && "page" in baseVariables && "pageSize" in baseVariables;
134
- const paginationVariables = usesPageBasedPagination
135
- ? {
136
- pageSize: paginationVars.first,
137
- page: Number(paginationVars.after ?? 0),
138
- }
139
- : { ...paginationVars };
140
- return {
141
- ...baseVariables,
142
- ...paginationVariables,
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 result = params.onUpdate(params.prev, typedResult);
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
- * Hook to stabilize variables based on deep equality comparison.
203
- * Prevents unnecessary re-fetches when parent components pass new variable objects
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 useStableVariables = (variables) => {
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 [stableVariables, setStableVariables];
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 Apollo Client directly.
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 from Apollo Client.
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
- // Use reducer to manage interconnected pagination state
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
- // Stabilize variables to prevent unnecessary re-fetches
301
- const [stableVariables, setStableVariables] = useStableVariables(props.variables);
302
- // Manage abort controller for cancellable requests
303
- const { abortSignal } = useAbortableRequest(props, setStableVariables);
304
- const internalProps = useMemo(() => {
305
- return {
306
- ...props,
307
- variables: stableVariables,
308
- };
309
- }, [props, stableVariables]);
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
- ...internalProps,
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
- ...internalProps.context,
317
+ ...props.context,
314
318
  fetchOptions: {
315
- ...internalProps.context?.fetchOptions,
316
- signal: abortSignal,
319
+ ...props.context?.fetchOptions,
320
+ signal: abortController.signal,
317
321
  },
318
322
  },
319
- pollInterval: internalProps.pollInterval,
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
- if (internalProps.onCompleted) {
327
- internalProps.onCompleted(completedData);
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 onReset = useCallback(() => {
337
- dispatch({ type: "INCREMENT_RESET_TRIGGER" });
338
- }, []);
339
- useEffect(() => {
340
- onReset();
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
- // Convert pagination variables based on pagination type (cursor vs page-based)
353
- const fetchMoreVariables = convertPaginationVariables(internalProps.variables, variables);
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
- onUpdate: internalProps.updateQuery,
360
- onDataUpdate: (data) => dispatch({ type: "SET_DATA", payload: data }),
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: setPageInfo,
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 (abortSignal.aborted) {
381
+ if (signal.aborted) {
371
382
  return;
372
383
  }
373
- if (internalProps.onError) {
374
- return internalProps.onError(error);
375
- }
376
- else {
377
- throw error;
384
+ if (onErrorRef.current) {
385
+ return onErrorRef.current(error);
378
386
  }
387
+ throw error;
379
388
  });
380
- }, [internalProps, setIsLoading, fetchMore, abortSignal, setPageInfo]);
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: firstRef.current,
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
- doFetchMoreRef.current(fetchVariables, undefined);
400
- // Refs are stable and don't need to trigger re-runs, but included to satisfy linter
401
- }, [stableVariables, state.resetTrigger, firstRef, doFetchMoreRef]);
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 fetchVariables = {
406
- first: firstRef.current,
407
- after,
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
- // Refs are stable and don't need to trigger re-runs, but included to satisfy linter
414
- }, [after, firstRef, lastRef, beforeRef, dataRef, doFetchMoreRef]);
415
- return useMemo(() => {
416
- return {
417
- data: state.data ?? previousData,
418
- previousData,
419
- loading: isLoading || lazyLoading,
420
- pagination: { setIsLoading, isLoading, setPageInfo, pageInfo, reset, nextPage, previousPage },
421
- lastFetchedData: state.lastFetchedData,
422
- reset,
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 { unionEdgesByNodeKey, useLazyQuery, usePaginationQuery, useQuery };
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.14.53",
3
+ "version": "1.15.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.51",
13
- "@trackunit/shared-utils": "1.13.60",
12
+ "@trackunit/i18n-library-translation": "1.12.52",
13
+ "@trackunit/shared-utils": "1.13.61",
14
14
  "es-toolkit": "^1.39.10",
15
- "@trackunit/react-components": "1.17.48"
15
+ "@trackunit/react-components": "1.18.0"
16
16
  },
17
17
  "module": "./index.esm.js",
18
18
  "main": "./index.cjs.js",
package/src/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export * from "./unionEdgesByNodeKey";
1
+ export type { UpdateQueryOptions } from "./paginationQueryUtils";
2
2
  export * from "./useLazyQuery";
3
3
  export * from "./usePaginationQuery";
4
4
  export * from "./useQuery";
@@ -1,10 +1,8 @@
1
- import { OperationVariables as ApolloOperationVariables } from "@apollo/client/core/types";
2
1
  import { RelayPageInfo } from "@trackunit/react-components";
3
- type RelayPaginationQueryVariables = {
4
- first?: number | null;
5
- last?: number | null;
6
- before?: string | null;
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
- onUpdate: (prev: TData | undefined, newData: TData) => {
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 Apollo Client directly.
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 from Apollo Client.
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>;