@trackunit/react-graphql-hooks 1.10.9 → 1.10.12

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
@@ -127,6 +127,121 @@ const useLazyQuery = (document, options) => {
127
127
  return react.useMemo(() => [executeQuery, enhancedResult], [executeQuery, enhancedResult]);
128
128
  };
129
129
 
130
+ /**
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.
133
+ */
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
+ };
146
+ };
147
+ /**
148
+ * Creates an update query handler for Apollo's fetchMore.
149
+ * Handles aborted requests and merges new data with existing data.
150
+ */
151
+ const createUpdateQueryHandler = (params) => {
152
+ return (_previousResult, { fetchMoreResult }) => {
153
+ // Handle aborted requests by returning previous data
154
+ if (params.abortSignal.aborted) {
155
+ // If prev does not hold any data we don't want to return it,
156
+ // since it will make the cache output an error to the console.
157
+ // https://github.com/apollographql/apollo-client/issues/8677
158
+ if (params.prev !== undefined && params.prev !== null) {
159
+ // Type assertion required: Apollo's updateQuery expects its own result type
160
+ return sharedUtils.objectKeys(params.prev).length === 0
161
+ ? undefined
162
+ : params.prev;
163
+ }
164
+ return undefined;
165
+ }
166
+ // Type assertion required: Apollo's fetchMoreResult has complex internal types
167
+ // that need to be cast to our TData generic for type-safe usage
168
+ const typedResult = fetchMoreResult;
169
+ params.onLastFetchedUpdate(typedResult);
170
+ const result = params.onUpdate(params.prev, typedResult);
171
+ params.onPageInfoUpdate(result.pageInfo ?? null);
172
+ params.onDataUpdate(result.data);
173
+ params.onLoadingComplete();
174
+ return result.data;
175
+ };
176
+ };
177
+ /**
178
+ * Creates a reducer function for managing pagination state.
179
+ * Handles data updates, reset, and reset trigger increments.
180
+ */
181
+ const createPaginationReducer = () => {
182
+ return (state, action) => {
183
+ switch (action.type) {
184
+ case "SET_DATA": {
185
+ return { ...state, data: action.payload };
186
+ }
187
+ case "SET_LAST_FETCHED_DATA": {
188
+ return { ...state, lastFetchedData: action.payload };
189
+ }
190
+ case "RESET": {
191
+ return { ...state, data: undefined, lastFetchedData: undefined };
192
+ }
193
+ case "INCREMENT_RESET_TRIGGER": {
194
+ return { ...state, resetTrigger: state.resetTrigger + 1 };
195
+ }
196
+ default: {
197
+ throw new Error(`${action} is not a known action type`);
198
+ }
199
+ }
200
+ };
201
+ };
202
+
203
+ /**
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.
207
+ */
208
+ const useStableVariables = (variables) => {
209
+ const [stableVariables, setStableVariables] = react.useState(variables);
210
+ reactComponents.useWatch({
211
+ value: variables,
212
+ onChange: setStableVariables,
213
+ skip: !Boolean(variables),
214
+ });
215
+ return stableVariables;
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 = () => {
222
+ const [abortController] = react.useState(() => new AbortController());
223
+ const firstRender = reactComponents.useIsFirstRender();
224
+ react.useEffect(() => {
225
+ if (firstRender) {
226
+ return;
227
+ }
228
+ return () => {
229
+ abortController.abort();
230
+ };
231
+ }, [abortController, firstRender]);
232
+ return { abortSignal: abortController.signal };
233
+ };
234
+ /**
235
+ * Hook to sync a value to a ref and return the ref.
236
+ * Useful for avoiding dependency cycles in effects.
237
+ */
238
+ const useSyncedRef = (value) => {
239
+ const ref = react.useRef(value);
240
+ react.useEffect(() => {
241
+ ref.current = value;
242
+ }, [value]);
243
+ return ref;
244
+ };
130
245
  /**
131
246
  * This hook is used to fetch data from a GraphQL query and support pagination, it will help maintain the data and the pagination.
132
247
  *
@@ -137,17 +252,15 @@ const useLazyQuery = (document, options) => {
137
252
  * lastFetchedData is used to store the last fetched data, so you can update data according to last page.
138
253
  *
139
254
  * @example
140
- *
141
255
  * const {
142
256
  * data,
143
257
  * loading,
144
258
  * pagination,
145
259
  * lastFetchedData,
146
- *} = usePaginationQuery({
147
- * query: myLazyQuery, // <-- myLazyQuery is the graphql LazyQuery to run pagination for.
260
+ * } = usePaginationQuery(MyAssetsDocument, {
148
261
  * updateQuery: (previous, newData) => {
149
- * // Here you can use the unionEdgesByNodeKey utils to merge your edges together.
150
- * if (newData?.assets?.edges) {
262
+ * // Here you can use the unionEdgesByNodeKey utils to merge your edges together.
263
+ * if (newData?.assets?.edges) {
151
264
  * return {
152
265
  * data: {
153
266
  * ...newData,
@@ -162,40 +275,23 @@ const useLazyQuery = (document, options) => {
162
275
  * return { data: newData, pageInfo: newData.assets.pageInfo };
163
276
  * },
164
277
  * });
165
- * @template TData
166
- * @template TVariables
167
- * @param {TypedDocumentNode<TData, TVariables>} document - The GraphQL document (query or mutation).
168
- * @param {PaginationQueryProps<TData, TVariables>} 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.
169
- * @returns {*} {PaginationQuery}
278
+ * @template TData - The type of the query result data.
279
+ * @template TVariables - The type of the query variables.
280
+ * @param document - The GraphQL query document.
281
+ * @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.
282
+ * @returns {PaginationQuery<TData>} The pagination query result containing data, loading state, pagination controls, and lastFetchedData.
170
283
  */
171
284
  const usePaginationQuery = (document, props) => {
172
- const [lastFetchedData, setLastFetchedData] = react.useState();
173
- const [resetTrigger, setResetTrigger] = react.useState(0);
174
- const [abortController, setAbortController] = react.useState(new AbortController());
175
- const [lastAbortController, setLastAbortController] = react.useState(undefined);
176
- // Makes **sure** query variables are stable.
177
- // Before, it required variables to always be memorized in the parent which was easy to forget and confusing.
178
- // Maybe better solution exists but this is the best I could come up with without changing to much.
179
- // If ever a better solution is found, please remove this ugliness.
180
- const [stableVariables, setStableVariables] = react.useState(props.variables);
181
- // Use ref for variables comparison to avoid setState inside useMemo
182
- const variablesRef = react.useRef(props.variables);
183
- // Use effect to update the ref when props.variables changes
184
- react.useEffect(() => {
185
- if (!esToolkit.isEqual(props.variables, variablesRef.current)) {
186
- variablesRef.current = props.variables;
187
- setStableVariables(props.variables);
188
- if (!props.skip) {
189
- if (lastAbortController) {
190
- lastAbortController.abort();
191
- }
192
- const newAbortController = new AbortController();
193
- setLastAbortController(newAbortController);
194
- setAbortController(newAbortController);
195
- }
196
- }
197
- // eslint-disable-next-line
198
- }, [props.variables]);
285
+ // Use reducer to manage interconnected pagination state
286
+ const [state, dispatch] = react.useReducer(createPaginationReducer(), {
287
+ data: undefined,
288
+ lastFetchedData: undefined,
289
+ resetTrigger: 0,
290
+ });
291
+ // Stabilize variables to prevent unnecessary re-fetches
292
+ const stableVariables = useStableVariables(props.variables);
293
+ // Manage abort controller for cancellable requests
294
+ const { abortSignal } = useAbortableRequest();
199
295
  const internalProps = react.useMemo(() => {
200
296
  return {
201
297
  ...props,
@@ -208,7 +304,7 @@ const usePaginationQuery = (document, props) => {
208
304
  ...internalProps.context,
209
305
  fetchOptions: {
210
306
  ...internalProps.context?.fetchOptions,
211
- signal: abortController.signal,
307
+ signal: abortSignal,
212
308
  },
213
309
  },
214
310
  pollInterval: internalProps.pollInterval,
@@ -216,7 +312,7 @@ const usePaginationQuery = (document, props) => {
216
312
  onCompleted: completedData => {
217
313
  if (networkStatus === client.NetworkStatus.refetch) {
218
314
  // trigger reset for refetchQueries for the provided document.
219
- setResetTrigger(prev => prev + 1);
315
+ dispatch({ type: "INCREMENT_RESET_TRIGGER" });
220
316
  }
221
317
  if (internalProps.onCompleted) {
222
318
  internalProps.onCompleted(completedData);
@@ -228,15 +324,14 @@ const usePaginationQuery = (document, props) => {
228
324
  nextFetchPolicy: "network-only",
229
325
  initialFetchPolicy: "network-only",
230
326
  });
231
- const [data, setData] = react.useState();
232
327
  const onReset = react.useCallback(() => {
233
- setResetTrigger(prev => prev + 1);
328
+ dispatch({ type: "INCREMENT_RESET_TRIGGER" });
234
329
  }, []);
235
330
  react.useEffect(() => {
236
331
  onReset();
237
332
  }, [document, onReset]);
238
333
  const { table: { setIsLoading, isLoading, setPageInfo, pageInfo, reset, nextPage, previousPage }, variables: { first, after, last, before }, } = reactComponents.useRelayPagination({
239
- pageSize: internalProps.pageSize || internalProps.variables?.first || reactComponents.defaultPageSize,
334
+ pageSize: internalProps.pageSize ?? internalProps.variables?.first ?? reactComponents.defaultPageSize,
240
335
  onReset,
241
336
  });
242
337
  const doFetchMore = react.useCallback((variables, prev) => {
@@ -245,47 +340,25 @@ const usePaginationQuery = (document, props) => {
245
340
  return;
246
341
  }
247
342
  setIsLoading(true);
248
- /**
249
- * To support pagination with page and pageSize, we need to convert the variables from first and after.
250
- */
251
- const fetchMoreVariables = {
252
- ...internalProps.variables,
253
- ...(internalProps.variables && "page" in internalProps.variables && "pageSize" in internalProps.variables
254
- ? {
255
- pageSize: variables.first,
256
- page: Number(variables.after) || 0,
257
- }
258
- : { ...variables }),
259
- };
343
+ // Convert pagination variables based on pagination type (cursor vs page-based)
344
+ const fetchMoreVariables = convertPaginationVariables(internalProps.variables, variables);
260
345
  fetchMore({
261
346
  variables: fetchMoreVariables,
262
- updateQuery: (previousResult, { fetchMoreResult }) => {
263
- if (abortController.signal.aborted) {
264
- // if prev does not hold any data we don't want to return it,
265
- // since it will make the cache output an error to the console.
266
- // https://github.com/apollographql/apollo-client/issues/8677
267
- if (prev) {
268
- return sharedUtils.objectKeys(prev).length === 0
269
- ? undefined
270
- : prev;
271
- }
272
- return undefined;
273
- }
274
- // Safely handle Apollo types
275
- const typedResult = fetchMoreResult;
276
- setLastFetchedData(typedResult);
277
- const result = internalProps.updateQuery(prev, typedResult);
278
- setPageInfo(result.pageInfo || null);
279
- setData(result.data);
280
- setIsLoading(false);
281
- return result.data;
282
- },
347
+ updateQuery: createUpdateQueryHandler({
348
+ abortSignal,
349
+ prev,
350
+ onUpdate: internalProps.updateQuery,
351
+ onDataUpdate: (data) => dispatch({ type: "SET_DATA", payload: data }),
352
+ onLastFetchedUpdate: (data) => dispatch({ type: "SET_LAST_FETCHED_DATA", payload: data }),
353
+ onPageInfoUpdate: setPageInfo,
354
+ onLoadingComplete: () => setIsLoading(false),
355
+ }),
283
356
  // It is apparently not possible to use the onError from the useLazyQuery hook so we have to handle it here.
284
357
  // However, if you need to pass in your own onError function, you can do so in the props of the hook.
285
358
  // But we ignore the error if the request was aborted.
286
359
  }).catch(error => {
287
360
  setIsLoading(false);
288
- if (abortController.signal.aborted) {
361
+ if (abortSignal.aborted) {
289
362
  return;
290
363
  }
291
364
  if (internalProps.onError) {
@@ -295,35 +368,18 @@ const usePaginationQuery = (document, props) => {
295
368
  throw error;
296
369
  }
297
370
  });
298
- }, [internalProps, setIsLoading, fetchMore, abortController, setPageInfo]);
371
+ }, [internalProps, setIsLoading, fetchMore, abortSignal, setPageInfo]);
299
372
  react.useEffect(() => {
300
- setData(undefined);
373
+ dispatch({ type: "RESET" });
301
374
  reset();
302
- }, [internalProps.variables, reset]);
303
- // Use a ref to track the current value of first variable
304
- const lastRef = react.useRef(last);
305
- react.useEffect(() => {
306
- lastRef.current = last;
307
- }, [last]);
308
- const beforeRef = react.useRef(before);
309
- react.useEffect(() => {
310
- beforeRef.current = before;
311
- }, [before]);
312
- const firstRef = react.useRef(first);
313
- react.useEffect(() => {
314
- firstRef.current = first;
315
- }, [first]);
316
- // Use a ref to track the current data
317
- const dataRef = react.useRef(data);
318
- react.useEffect(() => {
319
- dataRef.current = data;
320
- }, [data]);
321
- // Store doFetchMore in a ref to avoid dependency cycles
322
- const doFetchMoreRef = react.useRef(doFetchMore);
323
- react.useEffect(() => {
324
- doFetchMoreRef.current = doFetchMore;
325
- }, [doFetchMore]);
326
- // Stabilize the fetchMore call by using refs instead of direct dependencies
375
+ }, [stableVariables, reset]);
376
+ // Store pagination values in refs to avoid triggering unnecessary effect re-runs
377
+ const firstRef = useSyncedRef(first);
378
+ const lastRef = useSyncedRef(last);
379
+ const beforeRef = useSyncedRef(before);
380
+ const dataRef = useSyncedRef(state.data);
381
+ const doFetchMoreRef = useSyncedRef(doFetchMore);
382
+ // Fetch initial page when variables or reset trigger changes
327
383
  react.useEffect(() => {
328
384
  const fetchVariables = {
329
385
  first: firstRef.current,
@@ -331,30 +387,34 @@ const usePaginationQuery = (document, props) => {
331
387
  before: undefined,
332
388
  after: undefined,
333
389
  };
334
- // Use the ref version to avoid dependency cycles
335
390
  doFetchMoreRef.current(fetchVariables, undefined);
336
- // Removed doFetchMore from dependencies to prevent refetch loops
337
- }, [internalProps.variables, resetTrigger]);
338
- // Stabilize the pagination effect
391
+ // Refs are stable and don't need to trigger re-runs, but included to satisfy linter
392
+ }, [stableVariables, state.resetTrigger, firstRef, doFetchMoreRef]);
393
+ // Fetch next/previous page when after cursor changes
339
394
  react.useEffect(() => {
340
- if (after) {
341
- const fetchVariables = { first: firstRef.current, after, last: lastRef.current, before: beforeRef.current };
342
- // Use the ref version to avoid dependency cycles
395
+ if (after !== undefined && after !== null) {
396
+ const fetchVariables = {
397
+ first: firstRef.current,
398
+ after,
399
+ last: lastRef.current,
400
+ before: beforeRef.current,
401
+ };
343
402
  doFetchMoreRef.current(fetchVariables, dataRef.current);
344
403
  }
345
- // Removed doFetchMore from dependencies to prevent refetch loops
346
- }, [after]);
404
+ // Refs are stable and don't need to trigger re-runs, but included to satisfy linter
405
+ }, [after, firstRef, lastRef, beforeRef, dataRef, doFetchMoreRef]);
347
406
  return react.useMemo(() => {
348
407
  return {
349
- data: data || previousData,
408
+ data: state.data ?? previousData,
350
409
  previousData,
351
410
  loading: isLoading || lazyLoading,
352
411
  pagination: { setIsLoading, isLoading, setPageInfo, pageInfo, reset, nextPage, previousPage },
353
- lastFetchedData,
412
+ lastFetchedData: state.lastFetchedData,
354
413
  reset,
355
414
  };
356
415
  }, [
357
- data,
416
+ state.data,
417
+ state.lastFetchedData,
358
418
  isLoading,
359
419
  lazyLoading,
360
420
  setIsLoading,
@@ -363,7 +423,6 @@ const usePaginationQuery = (document, props) => {
363
423
  reset,
364
424
  nextPage,
365
425
  previousPage,
366
- lastFetchedData,
367
426
  previousData,
368
427
  ]);
369
428
  };
package/index.esm.js CHANGED
@@ -2,9 +2,9 @@ import 'react/jsx-runtime';
2
2
  import { registerTranslations } from '@trackunit/i18n-library-translation';
3
3
  import { truthy, objectKeys } from '@trackunit/shared-utils';
4
4
  import { useLazyQuery as useLazyQuery$1, NetworkStatus, useQuery as useQuery$1 } from '@apollo/client';
5
- import { omit, isEqual } from 'es-toolkit';
6
- import { useMemo, useState, useRef, useEffect, useCallback } from 'react';
7
- import { useRelayPagination, defaultPageSize } from '@trackunit/react-components';
5
+ import { omit } from 'es-toolkit';
6
+ import { useMemo, useReducer, useCallback, useEffect, useState, useRef } from 'react';
7
+ import { useRelayPagination, defaultPageSize, useWatch, useIsFirstRender } from '@trackunit/react-components';
8
8
 
9
9
  var defaultTranslations = {
10
10
 
@@ -125,6 +125,121 @@ const useLazyQuery = (document, options) => {
125
125
  return useMemo(() => [executeQuery, enhancedResult], [executeQuery, enhancedResult]);
126
126
  };
127
127
 
128
+ /**
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.
131
+ */
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
+ };
144
+ };
145
+ /**
146
+ * Creates an update query handler for Apollo's fetchMore.
147
+ * Handles aborted requests and merges new data with existing data.
148
+ */
149
+ const createUpdateQueryHandler = (params) => {
150
+ return (_previousResult, { fetchMoreResult }) => {
151
+ // Handle aborted requests by returning previous data
152
+ if (params.abortSignal.aborted) {
153
+ // If prev does not hold any data we don't want to return it,
154
+ // since it will make the cache output an error to the console.
155
+ // https://github.com/apollographql/apollo-client/issues/8677
156
+ if (params.prev !== undefined && params.prev !== null) {
157
+ // Type assertion required: Apollo's updateQuery expects its own result type
158
+ return objectKeys(params.prev).length === 0
159
+ ? undefined
160
+ : params.prev;
161
+ }
162
+ return undefined;
163
+ }
164
+ // Type assertion required: Apollo's fetchMoreResult has complex internal types
165
+ // that need to be cast to our TData generic for type-safe usage
166
+ const typedResult = fetchMoreResult;
167
+ params.onLastFetchedUpdate(typedResult);
168
+ const result = params.onUpdate(params.prev, typedResult);
169
+ params.onPageInfoUpdate(result.pageInfo ?? null);
170
+ params.onDataUpdate(result.data);
171
+ params.onLoadingComplete();
172
+ return result.data;
173
+ };
174
+ };
175
+ /**
176
+ * Creates a reducer function for managing pagination state.
177
+ * Handles data updates, reset, and reset trigger increments.
178
+ */
179
+ const createPaginationReducer = () => {
180
+ return (state, action) => {
181
+ switch (action.type) {
182
+ case "SET_DATA": {
183
+ return { ...state, data: action.payload };
184
+ }
185
+ case "SET_LAST_FETCHED_DATA": {
186
+ return { ...state, lastFetchedData: action.payload };
187
+ }
188
+ case "RESET": {
189
+ return { ...state, data: undefined, lastFetchedData: undefined };
190
+ }
191
+ case "INCREMENT_RESET_TRIGGER": {
192
+ return { ...state, resetTrigger: state.resetTrigger + 1 };
193
+ }
194
+ default: {
195
+ throw new Error(`${action} is not a known action type`);
196
+ }
197
+ }
198
+ };
199
+ };
200
+
201
+ /**
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.
205
+ */
206
+ const useStableVariables = (variables) => {
207
+ const [stableVariables, setStableVariables] = useState(variables);
208
+ useWatch({
209
+ value: variables,
210
+ onChange: setStableVariables,
211
+ skip: !Boolean(variables),
212
+ });
213
+ return stableVariables;
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 = () => {
220
+ const [abortController] = useState(() => new AbortController());
221
+ const firstRender = useIsFirstRender();
222
+ useEffect(() => {
223
+ if (firstRender) {
224
+ return;
225
+ }
226
+ return () => {
227
+ abortController.abort();
228
+ };
229
+ }, [abortController, firstRender]);
230
+ return { abortSignal: abortController.signal };
231
+ };
232
+ /**
233
+ * Hook to sync a value to a ref and return the ref.
234
+ * Useful for avoiding dependency cycles in effects.
235
+ */
236
+ const useSyncedRef = (value) => {
237
+ const ref = useRef(value);
238
+ useEffect(() => {
239
+ ref.current = value;
240
+ }, [value]);
241
+ return ref;
242
+ };
128
243
  /**
129
244
  * This hook is used to fetch data from a GraphQL query and support pagination, it will help maintain the data and the pagination.
130
245
  *
@@ -135,17 +250,15 @@ const useLazyQuery = (document, options) => {
135
250
  * lastFetchedData is used to store the last fetched data, so you can update data according to last page.
136
251
  *
137
252
  * @example
138
- *
139
253
  * const {
140
254
  * data,
141
255
  * loading,
142
256
  * pagination,
143
257
  * lastFetchedData,
144
- *} = usePaginationQuery({
145
- * query: myLazyQuery, // <-- myLazyQuery is the graphql LazyQuery to run pagination for.
258
+ * } = usePaginationQuery(MyAssetsDocument, {
146
259
  * updateQuery: (previous, newData) => {
147
- * // Here you can use the unionEdgesByNodeKey utils to merge your edges together.
148
- * if (newData?.assets?.edges) {
260
+ * // Here you can use the unionEdgesByNodeKey utils to merge your edges together.
261
+ * if (newData?.assets?.edges) {
149
262
  * return {
150
263
  * data: {
151
264
  * ...newData,
@@ -160,40 +273,23 @@ const useLazyQuery = (document, options) => {
160
273
  * return { data: newData, pageInfo: newData.assets.pageInfo };
161
274
  * },
162
275
  * });
163
- * @template TData
164
- * @template TVariables
165
- * @param {TypedDocumentNode<TData, TVariables>} document - The GraphQL document (query or mutation).
166
- * @param {PaginationQueryProps<TData, TVariables>} 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.
167
- * @returns {*} {PaginationQuery}
276
+ * @template TData - The type of the query result data.
277
+ * @template TVariables - The type of the query variables.
278
+ * @param document - The GraphQL query document.
279
+ * @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.
280
+ * @returns {PaginationQuery<TData>} The pagination query result containing data, loading state, pagination controls, and lastFetchedData.
168
281
  */
169
282
  const usePaginationQuery = (document, props) => {
170
- const [lastFetchedData, setLastFetchedData] = useState();
171
- const [resetTrigger, setResetTrigger] = useState(0);
172
- const [abortController, setAbortController] = useState(new AbortController());
173
- const [lastAbortController, setLastAbortController] = useState(undefined);
174
- // Makes **sure** query variables are stable.
175
- // Before, it required variables to always be memorized in the parent which was easy to forget and confusing.
176
- // Maybe better solution exists but this is the best I could come up with without changing to much.
177
- // If ever a better solution is found, please remove this ugliness.
178
- const [stableVariables, setStableVariables] = useState(props.variables);
179
- // Use ref for variables comparison to avoid setState inside useMemo
180
- const variablesRef = useRef(props.variables);
181
- // Use effect to update the ref when props.variables changes
182
- useEffect(() => {
183
- if (!isEqual(props.variables, variablesRef.current)) {
184
- variablesRef.current = props.variables;
185
- setStableVariables(props.variables);
186
- if (!props.skip) {
187
- if (lastAbortController) {
188
- lastAbortController.abort();
189
- }
190
- const newAbortController = new AbortController();
191
- setLastAbortController(newAbortController);
192
- setAbortController(newAbortController);
193
- }
194
- }
195
- // eslint-disable-next-line
196
- }, [props.variables]);
283
+ // Use reducer to manage interconnected pagination state
284
+ const [state, dispatch] = useReducer(createPaginationReducer(), {
285
+ data: undefined,
286
+ lastFetchedData: undefined,
287
+ resetTrigger: 0,
288
+ });
289
+ // Stabilize variables to prevent unnecessary re-fetches
290
+ const stableVariables = useStableVariables(props.variables);
291
+ // Manage abort controller for cancellable requests
292
+ const { abortSignal } = useAbortableRequest();
197
293
  const internalProps = useMemo(() => {
198
294
  return {
199
295
  ...props,
@@ -206,7 +302,7 @@ const usePaginationQuery = (document, props) => {
206
302
  ...internalProps.context,
207
303
  fetchOptions: {
208
304
  ...internalProps.context?.fetchOptions,
209
- signal: abortController.signal,
305
+ signal: abortSignal,
210
306
  },
211
307
  },
212
308
  pollInterval: internalProps.pollInterval,
@@ -214,7 +310,7 @@ const usePaginationQuery = (document, props) => {
214
310
  onCompleted: completedData => {
215
311
  if (networkStatus === NetworkStatus.refetch) {
216
312
  // trigger reset for refetchQueries for the provided document.
217
- setResetTrigger(prev => prev + 1);
313
+ dispatch({ type: "INCREMENT_RESET_TRIGGER" });
218
314
  }
219
315
  if (internalProps.onCompleted) {
220
316
  internalProps.onCompleted(completedData);
@@ -226,15 +322,14 @@ const usePaginationQuery = (document, props) => {
226
322
  nextFetchPolicy: "network-only",
227
323
  initialFetchPolicy: "network-only",
228
324
  });
229
- const [data, setData] = useState();
230
325
  const onReset = useCallback(() => {
231
- setResetTrigger(prev => prev + 1);
326
+ dispatch({ type: "INCREMENT_RESET_TRIGGER" });
232
327
  }, []);
233
328
  useEffect(() => {
234
329
  onReset();
235
330
  }, [document, onReset]);
236
331
  const { table: { setIsLoading, isLoading, setPageInfo, pageInfo, reset, nextPage, previousPage }, variables: { first, after, last, before }, } = useRelayPagination({
237
- pageSize: internalProps.pageSize || internalProps.variables?.first || defaultPageSize,
332
+ pageSize: internalProps.pageSize ?? internalProps.variables?.first ?? defaultPageSize,
238
333
  onReset,
239
334
  });
240
335
  const doFetchMore = useCallback((variables, prev) => {
@@ -243,47 +338,25 @@ const usePaginationQuery = (document, props) => {
243
338
  return;
244
339
  }
245
340
  setIsLoading(true);
246
- /**
247
- * To support pagination with page and pageSize, we need to convert the variables from first and after.
248
- */
249
- const fetchMoreVariables = {
250
- ...internalProps.variables,
251
- ...(internalProps.variables && "page" in internalProps.variables && "pageSize" in internalProps.variables
252
- ? {
253
- pageSize: variables.first,
254
- page: Number(variables.after) || 0,
255
- }
256
- : { ...variables }),
257
- };
341
+ // Convert pagination variables based on pagination type (cursor vs page-based)
342
+ const fetchMoreVariables = convertPaginationVariables(internalProps.variables, variables);
258
343
  fetchMore({
259
344
  variables: fetchMoreVariables,
260
- updateQuery: (previousResult, { fetchMoreResult }) => {
261
- if (abortController.signal.aborted) {
262
- // if prev does not hold any data we don't want to return it,
263
- // since it will make the cache output an error to the console.
264
- // https://github.com/apollographql/apollo-client/issues/8677
265
- if (prev) {
266
- return objectKeys(prev).length === 0
267
- ? undefined
268
- : prev;
269
- }
270
- return undefined;
271
- }
272
- // Safely handle Apollo types
273
- const typedResult = fetchMoreResult;
274
- setLastFetchedData(typedResult);
275
- const result = internalProps.updateQuery(prev, typedResult);
276
- setPageInfo(result.pageInfo || null);
277
- setData(result.data);
278
- setIsLoading(false);
279
- return result.data;
280
- },
345
+ updateQuery: createUpdateQueryHandler({
346
+ abortSignal,
347
+ prev,
348
+ onUpdate: internalProps.updateQuery,
349
+ onDataUpdate: (data) => dispatch({ type: "SET_DATA", payload: data }),
350
+ onLastFetchedUpdate: (data) => dispatch({ type: "SET_LAST_FETCHED_DATA", payload: data }),
351
+ onPageInfoUpdate: setPageInfo,
352
+ onLoadingComplete: () => setIsLoading(false),
353
+ }),
281
354
  // It is apparently not possible to use the onError from the useLazyQuery hook so we have to handle it here.
282
355
  // However, if you need to pass in your own onError function, you can do so in the props of the hook.
283
356
  // But we ignore the error if the request was aborted.
284
357
  }).catch(error => {
285
358
  setIsLoading(false);
286
- if (abortController.signal.aborted) {
359
+ if (abortSignal.aborted) {
287
360
  return;
288
361
  }
289
362
  if (internalProps.onError) {
@@ -293,35 +366,18 @@ const usePaginationQuery = (document, props) => {
293
366
  throw error;
294
367
  }
295
368
  });
296
- }, [internalProps, setIsLoading, fetchMore, abortController, setPageInfo]);
369
+ }, [internalProps, setIsLoading, fetchMore, abortSignal, setPageInfo]);
297
370
  useEffect(() => {
298
- setData(undefined);
371
+ dispatch({ type: "RESET" });
299
372
  reset();
300
- }, [internalProps.variables, reset]);
301
- // Use a ref to track the current value of first variable
302
- const lastRef = useRef(last);
303
- useEffect(() => {
304
- lastRef.current = last;
305
- }, [last]);
306
- const beforeRef = useRef(before);
307
- useEffect(() => {
308
- beforeRef.current = before;
309
- }, [before]);
310
- const firstRef = useRef(first);
311
- useEffect(() => {
312
- firstRef.current = first;
313
- }, [first]);
314
- // Use a ref to track the current data
315
- const dataRef = useRef(data);
316
- useEffect(() => {
317
- dataRef.current = data;
318
- }, [data]);
319
- // Store doFetchMore in a ref to avoid dependency cycles
320
- const doFetchMoreRef = useRef(doFetchMore);
321
- useEffect(() => {
322
- doFetchMoreRef.current = doFetchMore;
323
- }, [doFetchMore]);
324
- // Stabilize the fetchMore call by using refs instead of direct dependencies
373
+ }, [stableVariables, reset]);
374
+ // Store pagination values in refs to avoid triggering unnecessary effect re-runs
375
+ const firstRef = useSyncedRef(first);
376
+ const lastRef = useSyncedRef(last);
377
+ const beforeRef = useSyncedRef(before);
378
+ const dataRef = useSyncedRef(state.data);
379
+ const doFetchMoreRef = useSyncedRef(doFetchMore);
380
+ // Fetch initial page when variables or reset trigger changes
325
381
  useEffect(() => {
326
382
  const fetchVariables = {
327
383
  first: firstRef.current,
@@ -329,30 +385,34 @@ const usePaginationQuery = (document, props) => {
329
385
  before: undefined,
330
386
  after: undefined,
331
387
  };
332
- // Use the ref version to avoid dependency cycles
333
388
  doFetchMoreRef.current(fetchVariables, undefined);
334
- // Removed doFetchMore from dependencies to prevent refetch loops
335
- }, [internalProps.variables, resetTrigger]);
336
- // Stabilize the pagination effect
389
+ // Refs are stable and don't need to trigger re-runs, but included to satisfy linter
390
+ }, [stableVariables, state.resetTrigger, firstRef, doFetchMoreRef]);
391
+ // Fetch next/previous page when after cursor changes
337
392
  useEffect(() => {
338
- if (after) {
339
- const fetchVariables = { first: firstRef.current, after, last: lastRef.current, before: beforeRef.current };
340
- // Use the ref version to avoid dependency cycles
393
+ if (after !== undefined && after !== null) {
394
+ const fetchVariables = {
395
+ first: firstRef.current,
396
+ after,
397
+ last: lastRef.current,
398
+ before: beforeRef.current,
399
+ };
341
400
  doFetchMoreRef.current(fetchVariables, dataRef.current);
342
401
  }
343
- // Removed doFetchMore from dependencies to prevent refetch loops
344
- }, [after]);
402
+ // Refs are stable and don't need to trigger re-runs, but included to satisfy linter
403
+ }, [after, firstRef, lastRef, beforeRef, dataRef, doFetchMoreRef]);
345
404
  return useMemo(() => {
346
405
  return {
347
- data: data || previousData,
406
+ data: state.data ?? previousData,
348
407
  previousData,
349
408
  loading: isLoading || lazyLoading,
350
409
  pagination: { setIsLoading, isLoading, setPageInfo, pageInfo, reset, nextPage, previousPage },
351
- lastFetchedData,
410
+ lastFetchedData: state.lastFetchedData,
352
411
  reset,
353
412
  };
354
413
  }, [
355
- data,
414
+ state.data,
415
+ state.lastFetchedData,
356
416
  isLoading,
357
417
  lazyLoading,
358
418
  setIsLoading,
@@ -361,7 +421,6 @@ const usePaginationQuery = (document, props) => {
361
421
  reset,
362
422
  nextPage,
363
423
  previousPage,
364
- lastFetchedData,
365
424
  previousData,
366
425
  ]);
367
426
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trackunit/react-graphql-hooks",
3
- "version": "1.10.9",
3
+ "version": "1.10.12",
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.9.10",
13
- "@trackunit/shared-utils": "1.11.7",
12
+ "@trackunit/i18n-library-translation": "1.9.13",
13
+ "@trackunit/shared-utils": "1.11.10",
14
14
  "es-toolkit": "^1.39.10",
15
- "@trackunit/react-components": "1.12.11"
15
+ "@trackunit/react-components": "1.13.0"
16
16
  },
17
17
  "module": "./index.esm.js",
18
18
  "main": "./index.cjs.js",
@@ -0,0 +1,53 @@
1
+ import { OperationVariables as ApolloOperationVariables } from "@apollo/client/core/types";
2
+ 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;
8
+ };
9
+ export type PaginationState<TData> = {
10
+ data: TData | undefined;
11
+ lastFetchedData: TData | undefined;
12
+ resetTrigger: number;
13
+ };
14
+ export type PaginationAction<TData> = {
15
+ type: "SET_DATA";
16
+ payload: TData;
17
+ } | {
18
+ type: "SET_LAST_FETCHED_DATA";
19
+ payload: TData;
20
+ } | {
21
+ type: "RESET";
22
+ } | {
23
+ type: "INCREMENT_RESET_TRIGGER";
24
+ };
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
+ /**
31
+ * Creates an update query handler for Apollo's fetchMore.
32
+ * Handles aborted requests and merges new data with existing data.
33
+ */
34
+ export declare const createUpdateQueryHandler: <TData, TPreviousResult>(params: {
35
+ abortSignal: AbortSignal;
36
+ prev: TData | undefined;
37
+ onUpdate: (prev: TData | undefined, newData: TData) => {
38
+ data: TData;
39
+ pageInfo?: RelayPageInfo | null;
40
+ };
41
+ onDataUpdate: (data: TData) => void;
42
+ onLastFetchedUpdate: (data: TData) => void;
43
+ onPageInfoUpdate: (pageInfo: RelayPageInfo | null) => void;
44
+ onLoadingComplete: () => void;
45
+ }) => (_previousResult: TPreviousResult, { fetchMoreResult }: {
46
+ fetchMoreResult?: unknown;
47
+ }) => TPreviousResult;
48
+ /**
49
+ * Creates a reducer function for managing pagination state.
50
+ * Handles data updates, reset, and reset trigger increments.
51
+ */
52
+ export declare const createPaginationReducer: <TData>() => (state: PaginationState<TData>, action: PaginationAction<TData>) => PaginationState<TData>;
53
+ export {};
@@ -45,17 +45,15 @@ export interface PaginationQueryProps<TData, TVariables extends ApolloOperationV
45
45
  * lastFetchedData is used to store the last fetched data, so you can update data according to last page.
46
46
  *
47
47
  * @example
48
- *
49
48
  * const {
50
49
  * data,
51
50
  * loading,
52
51
  * pagination,
53
52
  * lastFetchedData,
54
- *} = usePaginationQuery({
55
- * query: myLazyQuery, // <-- myLazyQuery is the graphql LazyQuery to run pagination for.
53
+ * } = usePaginationQuery(MyAssetsDocument, {
56
54
  * updateQuery: (previous, newData) => {
57
- * // Here you can use the unionEdgesByNodeKey utils to merge your edges together.
58
- * if (newData?.assets?.edges) {
55
+ * // Here you can use the unionEdgesByNodeKey utils to merge your edges together.
56
+ * if (newData?.assets?.edges) {
59
57
  * return {
60
58
  * data: {
61
59
  * ...newData,
@@ -70,11 +68,11 @@ export interface PaginationQueryProps<TData, TVariables extends ApolloOperationV
70
68
  * return { data: newData, pageInfo: newData.assets.pageInfo };
71
69
  * },
72
70
  * });
73
- * @template TData
74
- * @template TVariables
75
- * @param {TypedDocumentNode<TData, TVariables>} document - The GraphQL document (query or mutation).
76
- * @param {PaginationQueryProps<TData, TVariables>} 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.
77
- * @returns {*} {PaginationQuery}
71
+ * @template TData - The type of the query result data.
72
+ * @template TVariables - The type of the query variables.
73
+ * @param document - The GraphQL query document.
74
+ * @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.
75
+ * @returns {PaginationQuery<TData>} The pagination query result containing data, loading state, pagination controls, and lastFetchedData.
78
76
  */
79
77
  export declare const usePaginationQuery: <TData, TVariables extends ApolloOperationVariables>(document: TypedDocumentNode<TData, TVariables>, props: PaginationQueryProps<TData, TVariables>) => PaginationQuery<TData>;
80
78
  export {};