@trackunit/react-graphql-hooks 1.15.8 → 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 +92 -69
- package/index.esm.js +93 -70
- package/package.json +1 -1
- package/src/index.d.ts +1 -1
- package/src/usePaginationQuery.d.ts +21 -2
- package/src/utils/createPaginationReducer.d.ts +21 -0
- package/src/{paginationQueryUtils.d.ts → utils/createUpdateQueryHandler.d.ts} +2 -23
- package/src/utils/useQueryVariableNames.d.ts +22 -0
- package/src/utils/useStableVariablesWithAbort.d.ts +8 -0
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
|
-
*
|
|
183
|
-
*
|
|
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
|
|
186
|
-
return (
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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 =
|
|
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
|
|
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
|
|
438
|
-
//
|
|
439
|
-
|
|
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 (!
|
|
462
|
+
if (!isInitializedRef.current) {
|
|
442
463
|
return;
|
|
443
464
|
}
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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
|
-
}, [
|
|
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
|
|
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
|
-
*
|
|
181
|
-
*
|
|
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
|
|
184
|
-
return (
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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 =
|
|
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
|
|
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
|
|
436
|
-
//
|
|
437
|
-
|
|
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 (!
|
|
460
|
+
if (!isInitializedRef.current) {
|
|
440
461
|
return;
|
|
441
462
|
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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
|
-
}, [
|
|
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
package/src/index.d.ts
CHANGED
|
@@ -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 "./
|
|
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 "
|
|
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
|
+
};
|