@trackunit/react-graphql-hooks 1.15.7 → 1.15.9

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
@@ -104,6 +104,32 @@ const useLazyQuery = (document, options) => {
104
104
  return react.useMemo(() => [executeQuery, enhancedResult], [executeQuery, enhancedResult]);
105
105
  };
106
106
 
107
+ /**
108
+ * Creates a reducer function for managing pagination state.
109
+ * Handles data updates, reset, and reset trigger increments.
110
+ */
111
+ const createPaginationReducer = () => {
112
+ return (state, action) => {
113
+ switch (action.type) {
114
+ case "SET_DATA": {
115
+ return { ...state, data: action.payload };
116
+ }
117
+ case "SET_LAST_FETCHED_DATA": {
118
+ return { ...state, lastFetchedData: action.payload };
119
+ }
120
+ case "RESET": {
121
+ return { ...state, data: undefined, lastFetchedData: undefined };
122
+ }
123
+ case "INCREMENT_RESET_TRIGGER": {
124
+ return { ...state, resetTrigger: state.resetTrigger + 1 };
125
+ }
126
+ default: {
127
+ throw new Error(`${action} is not a known action type`);
128
+ }
129
+ }
130
+ };
131
+ };
132
+
107
133
  /**
108
134
  * Creates a union of two arrays of edges by a key on the node.
109
135
  *
@@ -144,13 +170,9 @@ const createDirectionAwareUnion = (direction) => {
144
170
  */
145
171
  const createUpdateQueryHandler = (params) => {
146
172
  return (_previousResult, { fetchMoreResult }) => {
147
- // Handle aborted requests by returning previous data
148
173
  if (params.abortSignal.aborted) {
149
- // If prev does not hold any data we don't want to return it,
150
- // since it will make the cache output an error to the console.
151
174
  // https://github.com/apollographql/apollo-client/issues/8677
152
175
  if (params.prev !== undefined && params.prev !== null) {
153
- // Type assertion required: Apollo's updateQuery expects its own result type
154
176
  return sharedUtils.objectKeys(params.prev).length === 0
155
177
  ? undefined
156
178
  : params.prev;
@@ -178,30 +200,27 @@ const createUpdateQueryHandler = (params) => {
178
200
  return result.data;
179
201
  };
180
202
  };
203
+
181
204
  /**
182
- * Creates a reducer function for managing pagination state.
183
- * Handles data updates, reset, and reset trigger increments.
205
+ * Extracts declared variable names from a GraphQL document so the hook
206
+ * only injects relay pagination variables the query actually accepts.
207
+ *
208
+ * Finds the first OperationDefinition in the document regardless of
209
+ * position — this handles documents where fragment definitions appear
210
+ * before the query.
184
211
  */
185
- const createPaginationReducer = () => {
186
- return (state, action) => {
187
- switch (action.type) {
188
- case "SET_DATA": {
189
- return { ...state, data: action.payload };
190
- }
191
- case "SET_LAST_FETCHED_DATA": {
192
- return { ...state, lastFetchedData: action.payload };
193
- }
194
- case "RESET": {
195
- return { ...state, data: undefined, lastFetchedData: undefined };
196
- }
197
- case "INCREMENT_RESET_TRIGGER": {
198
- return { ...state, resetTrigger: state.resetTrigger + 1 };
199
- }
200
- default: {
201
- throw new Error(`${action} is not a known action type`);
202
- }
212
+ const useQueryVariableNames = (document) => {
213
+ return react.useMemo(() => {
214
+ const definition = document.definitions.find(def => def.kind === "OperationDefinition");
215
+ if (definition === undefined) {
216
+ return new Set();
203
217
  }
204
- };
218
+ const varDefs = definition.variableDefinitions;
219
+ if (varDefs === undefined) {
220
+ return new Set();
221
+ }
222
+ return new Set(varDefs.map(v => v.variable.name.value));
223
+ }, [document]);
205
224
  };
206
225
 
207
226
  /**
@@ -220,23 +239,9 @@ const useStableVariablesWithAbort = (variables, skip = false) => {
220
239
  setAbortController(new AbortController());
221
240
  }
222
241
  }
223
- return { stableVariables, abortController };
224
- };
225
- /**
226
- * Extracts declared variable names from a GraphQL document so the hook
227
- * only injects relay pagination variables the query actually accepts.
228
- */
229
- const getQueryVariableNames = (document) => {
230
- const definition = document.definitions[0];
231
- if (definition === undefined || definition.kind !== "OperationDefinition") {
232
- return new Set();
233
- }
234
- const varDefs = definition.variableDefinitions;
235
- if (varDefs === undefined) {
236
- return new Set();
237
- }
238
- return new Set(varDefs.map(v => v.variable.name.value));
242
+ return react.useMemo(() => ({ stableVariables, abortController }), [stableVariables, abortController]);
239
243
  };
244
+
240
245
  /**
241
246
  * `usePaginationQuery` fetches data from a GraphQL query with Relay-style cursor pagination.
242
247
  * It manages page accumulation, loading state, and provides `pagination` controls (including `fetchNext`)
@@ -252,7 +257,8 @@ const getQueryVariableNames = (document) => {
252
257
  * ### When not to use
253
258
  * Do not use usePaginationQuery for single-entity queries without pagination — use `useQuery` from this library directly.
254
259
  *
255
- * @example
260
+ * @example basic usage
261
+ * ```tsx
256
262
  * const {
257
263
  * data,
258
264
  * loading,
@@ -275,6 +281,24 @@ const getQueryVariableNames = (document) => {
275
281
  * return { data: newData, pageInfo: newData.assets.pageInfo };
276
282
  * },
277
283
  * });
284
+ * ```
285
+ * @example pagination example with url sync
286
+ * ```tsx
287
+ * const { initialCursor, updateCursor } = useCursorUrlSync({ paramName: "myTableCursor" });
288
+ * const { data, pagination } = usePaginationQuery(GetAssetsDocument, {
289
+ * initialCursor: initialCursor,
290
+ * variables: {
291
+ * after: undefined,
292
+ * before: undefined,
293
+ * first: pageSize,
294
+ * last: undefined,
295
+ * },
296
+ * });
297
+ *
298
+ * useBidirectionalScroll({
299
+ * onTopItemChange: updateCursor,
300
+ * });
301
+ * ```
278
302
  * @template TData - The type of the query result data.
279
303
  * @template TVariables - The type of the query variables.
280
304
  * @param document - The GraphQL query document.
@@ -282,8 +306,8 @@ const getQueryVariableNames = (document) => {
282
306
  * @returns {PaginationQuery<TData>} The pagination query result containing data, loading state, pagination controls, and lastFetchedData.
283
307
  */
284
308
  const usePaginationQuery = (document, props) => {
285
- const pageSize = props.pageSize ?? props.variables?.first ?? reactComponents.defaultPageSize;
286
- const declaredVarNames = react.useMemo(() => getQueryVariableNames(document), [document]);
309
+ const pageSize = react.useMemo(() => props.pageSize ?? props.variables?.first ?? reactComponents.defaultPageSize, [props.pageSize, props.variables]);
310
+ const declaredVarNames = useQueryVariableNames(document);
287
311
  const { stableVariables, abortController } = useStableVariablesWithAbort(props.variables, props.skip);
288
312
  const [state, dispatch] = react.useReducer(createPaginationReducer(), {
289
313
  data: undefined,
@@ -306,10 +330,14 @@ const usePaginationQuery = (document, props) => {
306
330
  // we must not call reset() because it wipes the initialCursor that
307
331
  // useRelayPagination is holding.
308
332
  const hasLoadedDataRef = react.useRef(false);
333
+ const isInitializedRef = react.useRef(false);
309
334
  const onReset = react.useCallback(() => {
310
335
  dispatch({ type: "INCREMENT_RESET_TRIGGER" });
311
336
  }, []);
312
337
  react.useEffect(() => {
338
+ if (!isInitializedRef.current) {
339
+ return;
340
+ }
313
341
  onReset();
314
342
  }, [document, onReset]);
315
343
  const { table: { setIsLoading, isLoading, setPageInfo, pageInfo, reset, nextPage, previousPage }, variables: { first, after, last, before }, } = reactComponents.useRelayPagination({
@@ -318,8 +346,7 @@ const usePaginationQuery = (document, props) => {
318
346
  initialCursor: props.initialCursor,
319
347
  });
320
348
  const { skip: _skip, updateQuery: _uq, pageSize: _ps, initialCursor: _ic, variables: _vars, ...lazyQueryProps } = props;
321
- const networkStatusRef = react.useRef(0);
322
- const [, { previousData, fetchMore, networkStatus, loading: lazyLoading }] = useLazyQuery(document, {
349
+ const [, { previousData, fetchMore, loading: lazyLoading }] = useLazyQuery(document, {
323
350
  ...lazyQueryProps,
324
351
  variables: stableVariables !== undefined
325
352
  ? {
@@ -337,9 +364,6 @@ const usePaginationQuery = (document, props) => {
337
364
  },
338
365
  notifyOnNetworkStatusChange: true,
339
366
  onCompleted: completedData => {
340
- if (networkStatusRef.current === client.NetworkStatus.refetch) {
341
- dispatch({ type: "INCREMENT_RESET_TRIGGER" });
342
- }
343
367
  onCompletedRef.current?.(completedData);
344
368
  },
345
369
  // This is safe since we have no cache and want to control fetch outselves
@@ -347,9 +371,6 @@ const usePaginationQuery = (document, props) => {
347
371
  nextFetchPolicy: "standby",
348
372
  initialFetchPolicy: "standby",
349
373
  });
350
- react.useEffect(() => {
351
- networkStatusRef.current = networkStatus;
352
- }, [networkStatus]);
353
374
  const executeFetch = react.useCallback((variables, prev, direction) => {
354
375
  if (props.skip) {
355
376
  if (props.initialCursor === undefined) {
@@ -434,27 +455,29 @@ const usePaginationQuery = (document, props) => {
434
455
  latestRef.current.data = state.data;
435
456
  latestRef.current.executeFetch = executeFetch;
436
457
  });
437
- // Reset accumulated data when variables change (skip until first load
438
- // to preserve initialCursor)
439
- const pendingResetRef = react.useRef(false);
458
+ // Reset accumulated data when variables change. Before first load we only
459
+ // bump the trigger (preserving initialCursor); after first load we also
460
+ // clear data and reset relay pagination.
440
461
  react.useEffect(() => {
441
- if (!hasLoadedDataRef.current) {
462
+ if (!isInitializedRef.current) {
442
463
  return;
443
464
  }
444
- pendingResetRef.current = true;
445
- dispatch({ type: "RESET" });
446
- reset();
465
+ if (hasLoadedDataRef.current) {
466
+ hasLoadedDataRef.current = false;
467
+ dispatch({ type: "RESET" });
468
+ reset();
469
+ }
470
+ else {
471
+ dispatch({ type: "INCREMENT_RESET_TRIGGER" });
472
+ }
447
473
  }, [stableVariables, reset]);
448
- // Fetch initial page when variables or reset trigger changes.
449
- // When stableVariables changes after data is loaded, the reset effect above
450
- // also increments resetTrigger — which would cause this effect to fire twice.
451
- // The pendingReset guard skips the first (stableVariables-triggered) fire so
452
- // only the resetTrigger-triggered fire fetches.
453
474
  react.useEffect(() => {
454
- if (pendingResetRef.current) {
455
- pendingResetRef.current = false;
456
- return;
457
- }
475
+ isInitializedRef.current = true;
476
+ }, []);
477
+ // Fetch the initial page on mount and whenever resetTrigger changes.
478
+ // Variable changes flow through the reset effect above which increments
479
+ // resetTrigger, so stableVariables is intentionally NOT a dependency here.
480
+ react.useEffect(() => {
458
481
  const fetchVariables = {
459
482
  first: latestRef.current.first ?? pageSize,
460
483
  last: undefined,
@@ -462,7 +485,7 @@ const usePaginationQuery = (document, props) => {
462
485
  after: latestRef.current.after ?? undefined,
463
486
  };
464
487
  latestRef.current.executeFetch(fetchVariables, undefined, "initial");
465
- }, [stableVariables, state.resetTrigger, pageSize]);
488
+ }, [state.resetTrigger, pageSize]);
466
489
  // Fetch next/previous page when relay cursor changes
467
490
  react.useEffect(() => {
468
491
  if (!hasLoadedDataRef.current) {
package/index.esm.js CHANGED
@@ -2,7 +2,7 @@ import 'react/jsx-runtime';
2
2
  import { registerTranslations } from '@trackunit/i18n-library-translation';
3
3
  import { useLazyQuery as useLazyQuery$1, NetworkStatus, useQuery as useQuery$1 } from '@apollo/client';
4
4
  import { omit, isEqual } from 'es-toolkit';
5
- import { useMemo, useReducer, useRef, useEffect, useCallback, useState } from 'react';
5
+ import { useMemo, useState, useReducer, useRef, useEffect, useCallback } from 'react';
6
6
  import { defaultPageSize, useRelayPagination } from '@trackunit/react-components';
7
7
  import { truthy, objectKeys } from '@trackunit/shared-utils';
8
8
 
@@ -102,6 +102,32 @@ const useLazyQuery = (document, options) => {
102
102
  return useMemo(() => [executeQuery, enhancedResult], [executeQuery, enhancedResult]);
103
103
  };
104
104
 
105
+ /**
106
+ * Creates a reducer function for managing pagination state.
107
+ * Handles data updates, reset, and reset trigger increments.
108
+ */
109
+ const createPaginationReducer = () => {
110
+ return (state, action) => {
111
+ switch (action.type) {
112
+ case "SET_DATA": {
113
+ return { ...state, data: action.payload };
114
+ }
115
+ case "SET_LAST_FETCHED_DATA": {
116
+ return { ...state, lastFetchedData: action.payload };
117
+ }
118
+ case "RESET": {
119
+ return { ...state, data: undefined, lastFetchedData: undefined };
120
+ }
121
+ case "INCREMENT_RESET_TRIGGER": {
122
+ return { ...state, resetTrigger: state.resetTrigger + 1 };
123
+ }
124
+ default: {
125
+ throw new Error(`${action} is not a known action type`);
126
+ }
127
+ }
128
+ };
129
+ };
130
+
105
131
  /**
106
132
  * Creates a union of two arrays of edges by a key on the node.
107
133
  *
@@ -142,13 +168,9 @@ const createDirectionAwareUnion = (direction) => {
142
168
  */
143
169
  const createUpdateQueryHandler = (params) => {
144
170
  return (_previousResult, { fetchMoreResult }) => {
145
- // Handle aborted requests by returning previous data
146
171
  if (params.abortSignal.aborted) {
147
- // If prev does not hold any data we don't want to return it,
148
- // since it will make the cache output an error to the console.
149
172
  // https://github.com/apollographql/apollo-client/issues/8677
150
173
  if (params.prev !== undefined && params.prev !== null) {
151
- // Type assertion required: Apollo's updateQuery expects its own result type
152
174
  return objectKeys(params.prev).length === 0
153
175
  ? undefined
154
176
  : params.prev;
@@ -176,30 +198,27 @@ const createUpdateQueryHandler = (params) => {
176
198
  return result.data;
177
199
  };
178
200
  };
201
+
179
202
  /**
180
- * Creates a reducer function for managing pagination state.
181
- * Handles data updates, reset, and reset trigger increments.
203
+ * Extracts declared variable names from a GraphQL document so the hook
204
+ * only injects relay pagination variables the query actually accepts.
205
+ *
206
+ * Finds the first OperationDefinition in the document regardless of
207
+ * position — this handles documents where fragment definitions appear
208
+ * before the query.
182
209
  */
183
- const createPaginationReducer = () => {
184
- return (state, action) => {
185
- switch (action.type) {
186
- case "SET_DATA": {
187
- return { ...state, data: action.payload };
188
- }
189
- case "SET_LAST_FETCHED_DATA": {
190
- return { ...state, lastFetchedData: action.payload };
191
- }
192
- case "RESET": {
193
- return { ...state, data: undefined, lastFetchedData: undefined };
194
- }
195
- case "INCREMENT_RESET_TRIGGER": {
196
- return { ...state, resetTrigger: state.resetTrigger + 1 };
197
- }
198
- default: {
199
- throw new Error(`${action} is not a known action type`);
200
- }
210
+ const useQueryVariableNames = (document) => {
211
+ return useMemo(() => {
212
+ const definition = document.definitions.find(def => def.kind === "OperationDefinition");
213
+ if (definition === undefined) {
214
+ return new Set();
201
215
  }
202
- };
216
+ const varDefs = definition.variableDefinitions;
217
+ if (varDefs === undefined) {
218
+ return new Set();
219
+ }
220
+ return new Set(varDefs.map(v => v.variable.name.value));
221
+ }, [document]);
203
222
  };
204
223
 
205
224
  /**
@@ -218,23 +237,9 @@ const useStableVariablesWithAbort = (variables, skip = false) => {
218
237
  setAbortController(new AbortController());
219
238
  }
220
239
  }
221
- return { stableVariables, abortController };
222
- };
223
- /**
224
- * Extracts declared variable names from a GraphQL document so the hook
225
- * only injects relay pagination variables the query actually accepts.
226
- */
227
- const getQueryVariableNames = (document) => {
228
- const definition = document.definitions[0];
229
- if (definition === undefined || definition.kind !== "OperationDefinition") {
230
- return new Set();
231
- }
232
- const varDefs = definition.variableDefinitions;
233
- if (varDefs === undefined) {
234
- return new Set();
235
- }
236
- return new Set(varDefs.map(v => v.variable.name.value));
240
+ return useMemo(() => ({ stableVariables, abortController }), [stableVariables, abortController]);
237
241
  };
242
+
238
243
  /**
239
244
  * `usePaginationQuery` fetches data from a GraphQL query with Relay-style cursor pagination.
240
245
  * It manages page accumulation, loading state, and provides `pagination` controls (including `fetchNext`)
@@ -250,7 +255,8 @@ const getQueryVariableNames = (document) => {
250
255
  * ### When not to use
251
256
  * Do not use usePaginationQuery for single-entity queries without pagination — use `useQuery` from this library directly.
252
257
  *
253
- * @example
258
+ * @example basic usage
259
+ * ```tsx
254
260
  * const {
255
261
  * data,
256
262
  * loading,
@@ -273,6 +279,24 @@ const getQueryVariableNames = (document) => {
273
279
  * return { data: newData, pageInfo: newData.assets.pageInfo };
274
280
  * },
275
281
  * });
282
+ * ```
283
+ * @example pagination example with url sync
284
+ * ```tsx
285
+ * const { initialCursor, updateCursor } = useCursorUrlSync({ paramName: "myTableCursor" });
286
+ * const { data, pagination } = usePaginationQuery(GetAssetsDocument, {
287
+ * initialCursor: initialCursor,
288
+ * variables: {
289
+ * after: undefined,
290
+ * before: undefined,
291
+ * first: pageSize,
292
+ * last: undefined,
293
+ * },
294
+ * });
295
+ *
296
+ * useBidirectionalScroll({
297
+ * onTopItemChange: updateCursor,
298
+ * });
299
+ * ```
276
300
  * @template TData - The type of the query result data.
277
301
  * @template TVariables - The type of the query variables.
278
302
  * @param document - The GraphQL query document.
@@ -280,8 +304,8 @@ const getQueryVariableNames = (document) => {
280
304
  * @returns {PaginationQuery<TData>} The pagination query result containing data, loading state, pagination controls, and lastFetchedData.
281
305
  */
282
306
  const usePaginationQuery = (document, props) => {
283
- const pageSize = props.pageSize ?? props.variables?.first ?? defaultPageSize;
284
- const declaredVarNames = useMemo(() => getQueryVariableNames(document), [document]);
307
+ const pageSize = useMemo(() => props.pageSize ?? props.variables?.first ?? defaultPageSize, [props.pageSize, props.variables]);
308
+ const declaredVarNames = useQueryVariableNames(document);
285
309
  const { stableVariables, abortController } = useStableVariablesWithAbort(props.variables, props.skip);
286
310
  const [state, dispatch] = useReducer(createPaginationReducer(), {
287
311
  data: undefined,
@@ -304,10 +328,14 @@ const usePaginationQuery = (document, props) => {
304
328
  // we must not call reset() because it wipes the initialCursor that
305
329
  // useRelayPagination is holding.
306
330
  const hasLoadedDataRef = useRef(false);
331
+ const isInitializedRef = useRef(false);
307
332
  const onReset = useCallback(() => {
308
333
  dispatch({ type: "INCREMENT_RESET_TRIGGER" });
309
334
  }, []);
310
335
  useEffect(() => {
336
+ if (!isInitializedRef.current) {
337
+ return;
338
+ }
311
339
  onReset();
312
340
  }, [document, onReset]);
313
341
  const { table: { setIsLoading, isLoading, setPageInfo, pageInfo, reset, nextPage, previousPage }, variables: { first, after, last, before }, } = useRelayPagination({
@@ -316,8 +344,7 @@ const usePaginationQuery = (document, props) => {
316
344
  initialCursor: props.initialCursor,
317
345
  });
318
346
  const { skip: _skip, updateQuery: _uq, pageSize: _ps, initialCursor: _ic, variables: _vars, ...lazyQueryProps } = props;
319
- const networkStatusRef = useRef(0);
320
- const [, { previousData, fetchMore, networkStatus, loading: lazyLoading }] = useLazyQuery(document, {
347
+ const [, { previousData, fetchMore, loading: lazyLoading }] = useLazyQuery(document, {
321
348
  ...lazyQueryProps,
322
349
  variables: stableVariables !== undefined
323
350
  ? {
@@ -335,9 +362,6 @@ const usePaginationQuery = (document, props) => {
335
362
  },
336
363
  notifyOnNetworkStatusChange: true,
337
364
  onCompleted: completedData => {
338
- if (networkStatusRef.current === NetworkStatus.refetch) {
339
- dispatch({ type: "INCREMENT_RESET_TRIGGER" });
340
- }
341
365
  onCompletedRef.current?.(completedData);
342
366
  },
343
367
  // This is safe since we have no cache and want to control fetch outselves
@@ -345,9 +369,6 @@ const usePaginationQuery = (document, props) => {
345
369
  nextFetchPolicy: "standby",
346
370
  initialFetchPolicy: "standby",
347
371
  });
348
- useEffect(() => {
349
- networkStatusRef.current = networkStatus;
350
- }, [networkStatus]);
351
372
  const executeFetch = useCallback((variables, prev, direction) => {
352
373
  if (props.skip) {
353
374
  if (props.initialCursor === undefined) {
@@ -432,27 +453,29 @@ const usePaginationQuery = (document, props) => {
432
453
  latestRef.current.data = state.data;
433
454
  latestRef.current.executeFetch = executeFetch;
434
455
  });
435
- // Reset accumulated data when variables change (skip until first load
436
- // to preserve initialCursor)
437
- const pendingResetRef = useRef(false);
456
+ // Reset accumulated data when variables change. Before first load we only
457
+ // bump the trigger (preserving initialCursor); after first load we also
458
+ // clear data and reset relay pagination.
438
459
  useEffect(() => {
439
- if (!hasLoadedDataRef.current) {
460
+ if (!isInitializedRef.current) {
440
461
  return;
441
462
  }
442
- pendingResetRef.current = true;
443
- dispatch({ type: "RESET" });
444
- reset();
463
+ if (hasLoadedDataRef.current) {
464
+ hasLoadedDataRef.current = false;
465
+ dispatch({ type: "RESET" });
466
+ reset();
467
+ }
468
+ else {
469
+ dispatch({ type: "INCREMENT_RESET_TRIGGER" });
470
+ }
445
471
  }, [stableVariables, reset]);
446
- // Fetch initial page when variables or reset trigger changes.
447
- // When stableVariables changes after data is loaded, the reset effect above
448
- // also increments resetTrigger — which would cause this effect to fire twice.
449
- // The pendingReset guard skips the first (stableVariables-triggered) fire so
450
- // only the resetTrigger-triggered fire fetches.
451
472
  useEffect(() => {
452
- if (pendingResetRef.current) {
453
- pendingResetRef.current = false;
454
- return;
455
- }
473
+ isInitializedRef.current = true;
474
+ }, []);
475
+ // Fetch the initial page on mount and whenever resetTrigger changes.
476
+ // Variable changes flow through the reset effect above which increments
477
+ // resetTrigger, so stableVariables is intentionally NOT a dependency here.
478
+ useEffect(() => {
456
479
  const fetchVariables = {
457
480
  first: latestRef.current.first ?? pageSize,
458
481
  last: undefined,
@@ -460,7 +483,7 @@ const usePaginationQuery = (document, props) => {
460
483
  after: latestRef.current.after ?? undefined,
461
484
  };
462
485
  latestRef.current.executeFetch(fetchVariables, undefined, "initial");
463
- }, [stableVariables, state.resetTrigger, pageSize]);
486
+ }, [state.resetTrigger, pageSize]);
464
487
  // Fetch next/previous page when relay cursor changes
465
488
  useEffect(() => {
466
489
  if (!hasLoadedDataRef.current) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trackunit/react-graphql-hooks",
3
- "version": "1.15.7",
3
+ "version": "1.15.9",
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.55",
13
- "@trackunit/shared-utils": "1.13.64",
12
+ "@trackunit/i18n-library-translation": "1.12.56",
13
+ "@trackunit/shared-utils": "1.13.65",
14
14
  "es-toolkit": "^1.39.10",
15
- "@trackunit/react-components": "1.18.4"
15
+ "@trackunit/react-components": "1.18.5"
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 type { UpdateQueryOptions } from "./paginationQueryUtils";
2
1
  export * from "./useLazyQuery";
3
2
  export * from "./usePaginationQuery";
4
3
  export * from "./useQuery";
4
+ export type { UpdateQueryOptions } from "./utils/createUpdateQueryHandler";
@@ -1,6 +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
+ import { type UpdateQueryOptions } from "./utils/createUpdateQueryHandler";
4
4
  /**
5
5
  * This type is used to return the data from the query, the pagination object and the last fetched data.
6
6
  */
@@ -58,7 +58,8 @@ export interface PaginationQueryProps<TData, TVariables extends ApolloOperationV
58
58
  * ### When not to use
59
59
  * Do not use usePaginationQuery for single-entity queries without pagination — use `useQuery` from this library directly.
60
60
  *
61
- * @example
61
+ * @example basic usage
62
+ * ```tsx
62
63
  * const {
63
64
  * data,
64
65
  * loading,
@@ -81,6 +82,24 @@ export interface PaginationQueryProps<TData, TVariables extends ApolloOperationV
81
82
  * return { data: newData, pageInfo: newData.assets.pageInfo };
82
83
  * },
83
84
  * });
85
+ * ```
86
+ * @example pagination example with url sync
87
+ * ```tsx
88
+ * const { initialCursor, updateCursor } = useCursorUrlSync({ paramName: "myTableCursor" });
89
+ * const { data, pagination } = usePaginationQuery(GetAssetsDocument, {
90
+ * initialCursor: initialCursor,
91
+ * variables: {
92
+ * after: undefined,
93
+ * before: undefined,
94
+ * first: pageSize,
95
+ * last: undefined,
96
+ * },
97
+ * });
98
+ *
99
+ * useBidirectionalScroll({
100
+ * onTopItemChange: updateCursor,
101
+ * });
102
+ * ```
84
103
  * @template TData - The type of the query result data.
85
104
  * @template TVariables - The type of the query variables.
86
105
  * @param document - The GraphQL query document.
@@ -0,0 +1,21 @@
1
+ export type PaginationState<TData> = {
2
+ data: TData | undefined;
3
+ lastFetchedData: TData | undefined;
4
+ resetTrigger: number;
5
+ };
6
+ export type PaginationAction<TData> = {
7
+ type: "SET_DATA";
8
+ payload: TData;
9
+ } | {
10
+ type: "SET_LAST_FETCHED_DATA";
11
+ payload: TData;
12
+ } | {
13
+ type: "RESET";
14
+ } | {
15
+ type: "INCREMENT_RESET_TRIGGER";
16
+ };
17
+ /**
18
+ * Creates a reducer function for managing pagination state.
19
+ * Handles data updates, reset, and reset trigger increments.
20
+ */
21
+ export declare const createPaginationReducer: <TData>() => (state: PaginationState<TData>, action: PaginationAction<TData>) => PaginationState<TData>;
@@ -1,25 +1,9 @@
1
- import { RelayPageInfo } from "@trackunit/react-components";
2
- import { unionEdgesByNodeKey } from "./unionEdgesByNodeKey";
1
+ import type { RelayPageInfo } from "@trackunit/react-components";
2
+ import { unionEdgesByNodeKey } from "../unionEdgesByNodeKey";
3
3
  export type FetchDirection = "forward" | "backward" | "initial";
4
4
  export type UpdateQueryOptions = {
5
5
  readonly unionEdgesByNodeKey: typeof unionEdgesByNodeKey;
6
6
  };
7
- export type PaginationState<TData> = {
8
- data: TData | undefined;
9
- lastFetchedData: TData | undefined;
10
- resetTrigger: number;
11
- };
12
- export type PaginationAction<TData> = {
13
- type: "SET_DATA";
14
- payload: TData;
15
- } | {
16
- type: "SET_LAST_FETCHED_DATA";
17
- payload: TData;
18
- } | {
19
- type: "RESET";
20
- } | {
21
- type: "INCREMENT_RESET_TRIGGER";
22
- };
23
7
  /**
24
8
  * Creates an update query handler for Apollo's fetchMore.
25
9
  * Handles aborted requests and merges new data with existing data.
@@ -39,8 +23,3 @@ export declare const createUpdateQueryHandler: <TData, TPreviousResult>(params:
39
23
  }) => (_previousResult: TPreviousResult, { fetchMoreResult }: {
40
24
  fetchMoreResult?: unknown;
41
25
  }) => TPreviousResult;
42
- /**
43
- * Creates a reducer function for managing pagination state.
44
- * Handles data updates, reset, and reset trigger increments.
45
- */
46
- export declare const createPaginationReducer: <TData>() => (state: PaginationState<TData>, action: PaginationAction<TData>) => PaginationState<TData>;
@@ -0,0 +1,22 @@
1
+ type GraphQLDocument = {
2
+ readonly definitions: ReadonlyArray<{
3
+ readonly kind: string;
4
+ readonly variableDefinitions?: ReadonlyArray<{
5
+ readonly variable: {
6
+ readonly name: {
7
+ readonly value: string;
8
+ };
9
+ };
10
+ }>;
11
+ }>;
12
+ };
13
+ /**
14
+ * Extracts declared variable names from a GraphQL document so the hook
15
+ * only injects relay pagination variables the query actually accepts.
16
+ *
17
+ * Finds the first OperationDefinition in the document regardless of
18
+ * position — this handles documents where fragment definitions appear
19
+ * before the query.
20
+ */
21
+ export declare const useQueryVariableNames: (document: GraphQLDocument) => ReadonlySet<string>;
22
+ export {};
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Stabilizes variables via deep equality and manages an AbortController for
3
+ * cancelling in-flight requests when variables change.
4
+ */
5
+ export declare const useStableVariablesWithAbort: <TVariables>(variables: TVariables | undefined, skip?: boolean) => {
6
+ stableVariables: TVariables | undefined;
7
+ abortController: AbortController;
8
+ };