@spoosh/react 0.6.0 → 0.7.1

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/dist/index.mjs CHANGED
@@ -53,10 +53,11 @@ function createUseRead(options) {
53
53
  initialized: false,
54
54
  prevContext: null
55
55
  });
56
- if (controllerRef.current && controllerRef.current.queryKey !== queryKey) {
56
+ const baseQueryKeyChanged = controllerRef.current && controllerRef.current.baseQueryKey !== queryKey;
57
+ if (baseQueryKeyChanged) {
57
58
  lifecycleRef.current.prevContext = controllerRef.current.controller.getContext();
58
59
  }
59
- if (!controllerRef.current || controllerRef.current.queryKey !== queryKey) {
60
+ if (!controllerRef.current || baseQueryKeyChanged) {
60
61
  const controller2 = createOperationController({
61
62
  operationType: "read",
62
63
  path: pathSegments,
@@ -73,29 +74,36 @@ function createUseRead(options) {
73
74
  return method(fetchOpts);
74
75
  }
75
76
  });
76
- controllerRef.current = { controller: controller2, queryKey };
77
+ controllerRef.current = { controller: controller2, queryKey, baseQueryKey: queryKey };
77
78
  }
78
79
  const controller = controllerRef.current.controller;
79
80
  controller.setPluginOptions(pluginOpts);
80
- const state = useSyncExternalStore(
81
- controller.subscribe,
82
- controller.getState,
83
- controller.getState
81
+ const subscribe = useCallback(
82
+ (callback) => {
83
+ return controller.subscribe(callback);
84
+ },
85
+ [controller]
84
86
  );
87
+ const getSnapshot = useCallback(() => {
88
+ return controller.getState();
89
+ }, [controller]);
90
+ const state = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
85
91
  const [requestState, setRequestState] = useState(() => {
86
92
  const cachedEntry = stateManager.getCache(queryKey);
87
93
  const hasCachedData = cachedEntry?.state?.data !== void 0;
88
94
  return { isPending: enabled && !hasCachedData, error: void 0 };
89
95
  });
96
+ const [, forceUpdate] = useState(0);
90
97
  const abortRef = useRef(controller.abort);
91
98
  abortRef.current = controller.abort;
92
99
  const pluginOptsKey = JSON.stringify(pluginOpts);
93
100
  const tagsKey = JSON.stringify(tags);
94
101
  const executeWithTracking = useCallback(
95
- async (force = false) => {
102
+ async (force = false, overrideOptions) => {
96
103
  setRequestState((prev) => ({ ...prev, isPending: true }));
97
104
  try {
98
- const response = await controller.execute(void 0, { force });
105
+ const execOptions = overrideOptions ? { ...capturedCall.options ?? {}, ...overrideOptions } : void 0;
106
+ const response = await controller.execute(execOptions, { force });
99
107
  if (response.error) {
100
108
  setRequestState({ isPending: false, error: response.error });
101
109
  } else {
@@ -104,10 +112,10 @@ function createUseRead(options) {
104
112
  return response;
105
113
  } catch (err) {
106
114
  setRequestState({ isPending: false, error: err });
107
- throw err;
115
+ return { error: err };
108
116
  }
109
117
  },
110
- [controller]
118
+ [controller, capturedCall.options]
111
119
  );
112
120
  useEffect(() => {
113
121
  return () => {
@@ -142,9 +150,13 @@ function createUseRead(options) {
142
150
  }
143
151
  }
144
152
  );
153
+ const unsubRefetchAll = eventEmitter.on("refetchAll", () => {
154
+ executeWithTracking(true);
155
+ });
145
156
  return () => {
146
157
  unsubRefetch();
147
158
  unsubInvalidate();
159
+ unsubRefetchAll();
148
160
  };
149
161
  }, [queryKey, enabled, tagsKey]);
150
162
  useEffect(() => {
@@ -155,9 +167,84 @@ function createUseRead(options) {
155
167
  const abort = useCallback(() => {
156
168
  abortRef.current();
157
169
  }, []);
158
- const trigger = useCallback(() => {
159
- return executeWithTracking(true);
160
- }, [executeWithTracking]);
170
+ const trigger = useCallback(
171
+ async (triggerOptions) => {
172
+ const { force = false, ...overrideOptions } = triggerOptions ?? {};
173
+ const hasOverrides = Object.keys(overrideOptions).length > 0;
174
+ if (!hasOverrides) {
175
+ return executeWithTracking(force, void 0);
176
+ }
177
+ const mergedOptions = {
178
+ ...capturedCall.options ?? {},
179
+ ...overrideOptions
180
+ };
181
+ const newQueryKey = stateManager.createQueryKey({
182
+ path: pathSegments,
183
+ method: capturedCall.method,
184
+ options: mergedOptions
185
+ });
186
+ if (newQueryKey === controllerRef.current?.queryKey) {
187
+ return executeWithTracking(force, overrideOptions);
188
+ }
189
+ const params = mergedOptions?.params;
190
+ const newResolvedPath = resolvePath(pathSegments, params);
191
+ const newResolvedTags = resolveTags({ tags }, newResolvedPath);
192
+ const newController = createOperationController({
193
+ operationType: "read",
194
+ path: pathSegments,
195
+ method: capturedCall.method,
196
+ tags: newResolvedTags,
197
+ requestOptions: mergedOptions,
198
+ stateManager,
199
+ eventEmitter,
200
+ pluginExecutor,
201
+ hookId,
202
+ fetchFn: async (fetchOpts) => {
203
+ const pathMethods = api(capturedCall.path);
204
+ const method = pathMethods[capturedCall.method];
205
+ return method(fetchOpts);
206
+ }
207
+ });
208
+ newController.setPluginOptions(pluginOpts);
209
+ const currentBaseQueryKey = controllerRef.current?.baseQueryKey ?? queryKey;
210
+ controllerRef.current = {
211
+ controller: newController,
212
+ queryKey: newQueryKey,
213
+ baseQueryKey: currentBaseQueryKey
214
+ };
215
+ forceUpdate((n) => n + 1);
216
+ newController.mount();
217
+ setRequestState((prev) => ({ ...prev, isPending: true }));
218
+ try {
219
+ const response = await newController.execute(mergedOptions, {
220
+ force
221
+ });
222
+ if (response.error) {
223
+ setRequestState({ isPending: false, error: response.error });
224
+ } else {
225
+ setRequestState({ isPending: false, error: void 0 });
226
+ }
227
+ return response;
228
+ } catch (err) {
229
+ setRequestState({ isPending: false, error: err });
230
+ return { error: err };
231
+ }
232
+ },
233
+ [
234
+ executeWithTracking,
235
+ capturedCall.options,
236
+ capturedCall.method,
237
+ capturedCall.path,
238
+ pathSegments,
239
+ tags,
240
+ stateManager,
241
+ eventEmitter,
242
+ pluginExecutor,
243
+ hookId,
244
+ pluginOpts,
245
+ api
246
+ ]
247
+ );
161
248
  const entry = stateManager.getCache(queryKey);
162
249
  const pluginResultData = entry?.meta ? Object.fromEntries(entry.meta) : {};
163
250
  const opts = capturedCall.options;
@@ -280,7 +367,7 @@ function createUseWrite(options) {
280
367
  return response;
281
368
  } catch (err) {
282
369
  setRequestState({ isPending: false, error: err });
283
- throw err;
370
+ return { error: err };
284
371
  }
285
372
  },
286
373
  [selectedEndpoint.path]
@@ -315,160 +402,18 @@ function createUseWrite(options) {
315
402
  return useWrite;
316
403
  }
317
404
 
318
- // src/useLazyRead/index.ts
319
- import {
320
- useSyncExternalStore as useSyncExternalStore3,
321
- useRef as useRef3,
322
- useCallback as useCallback3,
323
- useState as useState3,
324
- useId as useId3
325
- } from "react";
326
- import {
327
- createOperationController as createOperationController3,
328
- createSelectorProxy as createSelectorProxy3,
329
- resolvePath as resolvePath3
330
- } from "@spoosh/core";
331
- function createUseLazyRead(options) {
332
- const { api, stateManager, pluginExecutor, eventEmitter } = options;
333
- function useLazyRead(readFn) {
334
- const hookId = useId3();
335
- const selectorResultRef = useRef3({
336
- call: null,
337
- selector: null
338
- });
339
- const selectorProxy = createSelectorProxy3((result) => {
340
- selectorResultRef.current = result;
341
- });
342
- readFn(selectorProxy);
343
- const selectedEndpoint = selectorResultRef.current.selector;
344
- if (!selectedEndpoint) {
345
- throw new Error(
346
- 'useLazyRead requires selecting an HTTP method (GET). Example: useLazyRead((api) => api("posts").GET)'
347
- );
348
- }
349
- if (selectedEndpoint.method !== "GET") {
350
- throw new Error(
351
- "useLazyRead only supports GET method. Use useWrite for POST, PUT, PATCH, DELETE methods."
352
- );
353
- }
354
- const pathSegments = selectedEndpoint.path.split("/").filter(Boolean);
355
- const controllerRef = useRef3(null);
356
- const emptyStateRef = useRef3({ data: void 0, error: void 0 });
357
- const [currentQueryKey, setCurrentQueryKey] = useState3(null);
358
- const [, forceUpdate] = useState3(0);
359
- const getOrCreateController = useCallback3(
360
- (triggerOptions) => {
361
- const queryKey = stateManager.createQueryKey({
362
- path: pathSegments,
363
- method: selectedEndpoint.method,
364
- options: triggerOptions
365
- });
366
- if (controllerRef.current?.queryKey === queryKey) {
367
- return { controller: controllerRef.current.controller, queryKey };
368
- }
369
- const controller2 = createOperationController3({
370
- operationType: "read",
371
- path: pathSegments,
372
- method: "GET",
373
- tags: [],
374
- stateManager,
375
- eventEmitter,
376
- pluginExecutor,
377
- hookId,
378
- requestOptions: triggerOptions,
379
- fetchFn: async (fetchOpts) => {
380
- const pathMethods = api(selectedEndpoint.path);
381
- const method = pathMethods[selectedEndpoint.method];
382
- return method(fetchOpts);
383
- }
384
- });
385
- controllerRef.current = { controller: controller2, queryKey };
386
- setCurrentQueryKey(queryKey);
387
- forceUpdate((n) => n + 1);
388
- return { controller: controller2, queryKey };
389
- },
390
- [pathSegments, selectedEndpoint.method, selectedEndpoint.path, hookId]
391
- );
392
- const controller = controllerRef.current?.controller;
393
- const subscribe = useCallback3(
394
- (callback) => {
395
- if (!controller) return () => {
396
- };
397
- return controller.subscribe(callback);
398
- },
399
- [controller]
400
- );
401
- const getSnapshot = useCallback3(() => {
402
- if (!controller) return emptyStateRef.current;
403
- return controller.getState();
404
- }, [controller]);
405
- const state = useSyncExternalStore3(subscribe, getSnapshot, getSnapshot);
406
- const [lastTriggerOptions, setLastTriggerOptions] = useState3(void 0);
407
- const [requestState, setRequestState] = useState3({ isPending: false, error: void 0 });
408
- const abort = useCallback3(() => {
409
- controllerRef.current?.controller.abort();
410
- }, []);
411
- const trigger = useCallback3(
412
- async (triggerOptions) => {
413
- setLastTriggerOptions(triggerOptions);
414
- setRequestState((prev) => ({ ...prev, isPending: true }));
415
- const params = triggerOptions?.params;
416
- resolvePath3(pathSegments, params);
417
- const { controller: ctrl } = getOrCreateController(triggerOptions);
418
- ctrl.setPluginOptions(triggerOptions);
419
- try {
420
- const response = await ctrl.execute(triggerOptions);
421
- if (response.error) {
422
- setRequestState({ isPending: false, error: response.error });
423
- } else {
424
- setRequestState({ isPending: false, error: void 0 });
425
- }
426
- return response;
427
- } catch (err) {
428
- setRequestState({ isPending: false, error: err });
429
- throw err;
430
- }
431
- },
432
- [pathSegments, getOrCreateController]
433
- );
434
- const opts = lastTriggerOptions;
435
- const inputInner = {};
436
- if (opts?.query !== void 0) {
437
- inputInner.query = opts.query;
438
- }
439
- if (opts?.body !== void 0) {
440
- inputInner.body = opts.body;
441
- }
442
- if (opts?.params !== void 0) {
443
- inputInner.params = opts.params;
444
- }
445
- const inputField = Object.keys(inputInner).length > 0 ? { input: inputInner } : {};
446
- const loading = requestState.isPending;
447
- return {
448
- trigger,
449
- ...inputField,
450
- data: state.data,
451
- error: requestState.error ?? state.error,
452
- loading,
453
- abort
454
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
455
- };
456
- }
457
- return useLazyRead;
458
- }
459
-
460
405
  // src/useInfiniteRead/index.ts
461
406
  import {
462
- useRef as useRef4,
407
+ useRef as useRef3,
463
408
  useEffect as useEffect2,
464
- useSyncExternalStore as useSyncExternalStore4,
465
- useId as useId4,
466
- useState as useState4
409
+ useSyncExternalStore as useSyncExternalStore3,
410
+ useId as useId3,
411
+ useState as useState3
467
412
  } from "react";
468
413
  import {
469
414
  createInfiniteReadController,
470
- createSelectorProxy as createSelectorProxy4,
471
- resolvePath as resolvePath4,
415
+ createSelectorProxy as createSelectorProxy3,
416
+ resolvePath as resolvePath3,
472
417
  resolveTags as resolveTags3
473
418
  } from "@spoosh/core";
474
419
  function createUseInfiniteRead(options) {
@@ -484,12 +429,12 @@ function createUseInfiniteRead(options) {
484
429
  prevPageRequest,
485
430
  ...pluginOpts
486
431
  } = readOptions;
487
- const hookId = useId4();
488
- const selectorResultRef = useRef4({
432
+ const hookId = useId3();
433
+ const selectorResultRef = useRef3({
489
434
  call: null,
490
435
  selector: null
491
436
  });
492
- const selectorProxy = createSelectorProxy4((result2) => {
437
+ const selectorProxy = createSelectorProxy3((result2) => {
493
438
  selectorResultRef.current = result2;
494
439
  });
495
440
  readFn(selectorProxy);
@@ -512,13 +457,13 @@ function createUseInfiniteRead(options) {
512
457
  params: void 0,
513
458
  body: void 0
514
459
  };
515
- const resolvedPath = resolvePath4(pathSegments, requestOptions?.params);
460
+ const resolvedPath = resolvePath3(pathSegments, requestOptions?.params);
516
461
  const resolvedTags = resolveTags3({ tags }, resolvedPath);
517
- const canFetchNextRef = useRef4(canFetchNext);
518
- const canFetchPrevRef = useRef4(canFetchPrev);
519
- const nextPageRequestRef = useRef4(nextPageRequest);
520
- const prevPageRequestRef = useRef4(prevPageRequest);
521
- const mergerRef = useRef4(merger);
462
+ const canFetchNextRef = useRef3(canFetchNext);
463
+ const canFetchPrevRef = useRef3(canFetchPrev);
464
+ const nextPageRequestRef = useRef3(nextPageRequest);
465
+ const prevPageRequestRef = useRef3(prevPageRequest);
466
+ const mergerRef = useRef3(merger);
522
467
  canFetchNextRef.current = canFetchNext;
523
468
  canFetchPrevRef.current = canFetchPrev;
524
469
  nextPageRequestRef.current = nextPageRequest;
@@ -529,7 +474,7 @@ function createUseInfiniteRead(options) {
529
474
  method: capturedCall.method,
530
475
  options: baseOptionsForKey
531
476
  });
532
- const controllerRef = useRef4(null);
477
+ const controllerRef = useRef3(null);
533
478
  if (!controllerRef.current || controllerRef.current.queryKey !== queryKey) {
534
479
  controllerRef.current = {
535
480
  controller: createInfiniteReadController({
@@ -565,12 +510,12 @@ function createUseInfiniteRead(options) {
565
510
  }
566
511
  const controller = controllerRef.current.controller;
567
512
  controller.setPluginOptions(pluginOpts);
568
- const state = useSyncExternalStore4(
513
+ const state = useSyncExternalStore3(
569
514
  controller.subscribe,
570
515
  controller.getState,
571
516
  controller.getState
572
517
  );
573
- const [isPending, setIsPending] = useState4(() => {
518
+ const [isPending, setIsPending] = useState3(() => {
574
519
  return enabled && state.data === void 0;
575
520
  });
576
521
  const fetchingDirection = controller.getFetchingDirection();
@@ -579,7 +524,7 @@ function createUseInfiniteRead(options) {
579
524
  const fetchingPrev = fetchingDirection === "prev";
580
525
  const hasData = state.data !== void 0;
581
526
  const loading = (isPending || fetching) && !hasData;
582
- const lifecycleRef = useRef4({
527
+ const lifecycleRef = useRef3({
583
528
  initialized: false,
584
529
  prevContext: null
585
530
  });
@@ -605,8 +550,13 @@ function createUseInfiniteRead(options) {
605
550
  }
606
551
  }
607
552
  );
553
+ const unsubRefetchAll = eventEmitter.on("refetchAll", () => {
554
+ setIsPending(true);
555
+ controller.refetch().finally(() => setIsPending(false));
556
+ });
608
557
  return () => {
609
558
  unsubInvalidate();
559
+ unsubRefetchAll();
610
560
  };
611
561
  }, [tagsKey]);
612
562
  useEffect2(() => {
@@ -662,12 +612,6 @@ function createReactSpoosh(instance) {
662
612
  eventEmitter,
663
613
  pluginExecutor
664
614
  });
665
- const useLazyRead = createUseLazyRead({
666
- api,
667
- stateManager,
668
- eventEmitter,
669
- pluginExecutor
670
- });
671
615
  const useInfiniteRead = createUseInfiniteRead({
672
616
  api,
673
617
  stateManager,
@@ -693,7 +637,6 @@ function createReactSpoosh(instance) {
693
637
  return {
694
638
  useRead,
695
639
  useWrite,
696
- useLazyRead,
697
640
  useInfiniteRead,
698
641
  ...instanceApis
699
642
  };
@@ -701,7 +644,6 @@ function createReactSpoosh(instance) {
701
644
  export {
702
645
  createReactSpoosh,
703
646
  createUseInfiniteRead,
704
- createUseLazyRead,
705
647
  createUseRead,
706
648
  createUseWrite
707
649
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spoosh/react",
3
- "version": "0.6.0",
3
+ "version": "0.7.1",
4
4
  "license": "MIT",
5
5
  "description": "React hooks for Spoosh API client",
6
6
  "keywords": [
@@ -38,7 +38,10 @@
38
38
  "react": "^18 || ^19"
39
39
  },
40
40
  "devDependencies": {
41
- "@spoosh/core": "0.9.0"
41
+ "@testing-library/react": "^16.0.0",
42
+ "jsdom": "^26.0.0",
43
+ "@spoosh/core": "0.9.3",
44
+ "@spoosh/test-utils": "0.1.5"
42
45
  },
43
46
  "scripts": {
44
47
  "dev": "tsup --watch",