@objectstack/client-react 17.0.0-rc.1 → 17.0.0-rc.3

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
@@ -25,14 +25,33 @@ function useClient() {
25
25
  }
26
26
 
27
27
  // src/data-hooks.tsx
28
- import { useState, useEffect, useCallback, useRef as useRef2 } from "react";
28
+ import { useState, useEffect as useEffect2, useCallback as useCallback2, useRef as useRef3 } from "react";
29
+
30
+ // src/internal-deps.ts
31
+ import { useCallback, useEffect, useRef as useRef2 } from "react";
32
+ function stableKey(input) {
33
+ if (input === null || typeof input !== "object") return JSON.stringify(input) ?? "undefined";
34
+ if (Array.isArray(input)) return "[" + input.map(stableKey).join(",") + "]";
35
+ const obj = input;
36
+ const keys = Object.keys(obj).sort();
37
+ return "{" + keys.map((k) => JSON.stringify(k) + ":" + stableKey(obj[k])).join(",") + "}";
38
+ }
39
+ function useEventCallback(fn) {
40
+ const ref = useRef2(fn);
41
+ useEffect(() => {
42
+ ref.current = fn;
43
+ }, [fn]);
44
+ return useCallback((...args) => ref.current?.(...args), []);
45
+ }
46
+
47
+ // src/data-hooks.tsx
29
48
  function useQuery(object, options = {}) {
30
49
  const client = useClient();
31
50
  const [data, setData] = useState(null);
32
51
  const [isLoading, setIsLoading] = useState(true);
33
52
  const [isRefetching, setIsRefetching] = useState(false);
34
53
  const [error, setError] = useState(null);
35
- const intervalRef = useRef2(void 0);
54
+ const intervalRef = useRef3(void 0);
36
55
  const {
37
56
  query,
38
57
  // Canonical names take precedence over legacy names
@@ -51,7 +70,17 @@ function useQuery(object, options = {}) {
51
70
  const resolvedSort = orderBy;
52
71
  const resolvedLimit = limit;
53
72
  const resolvedOffset = offset;
54
- const fetchData = useCallback(async (isRefetch = false) => {
73
+ const queryKey = stableKey({
74
+ query,
75
+ where: resolvedWhere,
76
+ fields: resolvedFields,
77
+ orderBy: resolvedSort,
78
+ limit: resolvedLimit,
79
+ offset: resolvedOffset
80
+ });
81
+ const handleSuccess = useEventCallback(onSuccess);
82
+ const handleError = useEventCallback(onError);
83
+ const fetchData = useCallback2(async (isRefetch = false) => {
55
84
  if (!enabled) return;
56
85
  try {
57
86
  if (isRefetch) {
@@ -73,20 +102,20 @@ function useQuery(object, options = {}) {
73
102
  });
74
103
  }
75
104
  setData(result);
76
- onSuccess?.(result);
105
+ handleSuccess(result);
77
106
  } catch (err) {
78
107
  const error2 = err instanceof Error ? err : new Error("Query failed");
79
108
  setError(error2);
80
- onError?.(error2);
109
+ handleError(error2);
81
110
  } finally {
82
111
  setIsLoading(false);
83
112
  setIsRefetching(false);
84
113
  }
85
- }, [client, object, query, resolvedFields, resolvedWhere, resolvedSort, resolvedLimit, resolvedOffset, enabled, onSuccess, onError]);
86
- useEffect(() => {
114
+ }, [client, object, queryKey, enabled, handleSuccess, handleError]);
115
+ useEffect2(() => {
87
116
  fetchData();
88
117
  }, [fetchData]);
89
- useEffect(() => {
118
+ useEffect2(() => {
90
119
  if (refetchInterval && enabled) {
91
120
  intervalRef.current = setInterval(() => {
92
121
  fetchData(true);
@@ -99,7 +128,7 @@ function useQuery(object, options = {}) {
99
128
  }
100
129
  return void 0;
101
130
  }, [refetchInterval, enabled, fetchData]);
102
- const refetch = useCallback(async () => {
131
+ const refetch = useCallback2(async () => {
103
132
  await fetchData(true);
104
133
  }, [fetchData]);
105
134
  return {
@@ -116,7 +145,7 @@ function useMutation(object, operation, options = {}) {
116
145
  const [isLoading, setIsLoading] = useState(false);
117
146
  const [error, setError] = useState(null);
118
147
  const { onSuccess, onError, onSettled } = options;
119
- const mutateAsync = useCallback(async (variables) => {
148
+ const mutateAsync = useCallback2(async (variables) => {
120
149
  setIsLoading(true);
121
150
  setError(null);
122
151
  try {
@@ -161,12 +190,12 @@ function useMutation(object, operation, options = {}) {
161
190
  setIsLoading(false);
162
191
  }
163
192
  }, [client, object, operation, onSuccess, onError, onSettled]);
164
- const mutate = useCallback((variables) => {
193
+ const mutate = useCallback2((variables) => {
165
194
  return mutateAsync(variables).catch(() => {
166
195
  return null;
167
196
  });
168
197
  }, [mutateAsync]);
169
- const reset = useCallback(() => {
198
+ const reset = useCallback2(() => {
170
199
  setData(null);
171
200
  setError(null);
172
201
  setIsLoading(false);
@@ -192,17 +221,17 @@ function usePagination(object, options = {}) {
192
221
  const totalPages = Math.ceil(totalCount / pageSize);
193
222
  const hasNextPage = page < totalPages;
194
223
  const hasPreviousPage = page > 1;
195
- const nextPage = useCallback(() => {
224
+ const nextPage = useCallback2(() => {
196
225
  if (hasNextPage) {
197
226
  setPage((p) => p + 1);
198
227
  }
199
228
  }, [hasNextPage]);
200
- const previousPage = useCallback(() => {
229
+ const previousPage = useCallback2(() => {
201
230
  if (hasPreviousPage) {
202
231
  setPage((p) => p - 1);
203
232
  }
204
233
  }, [hasPreviousPage]);
205
- const goToPage = useCallback((newPage) => {
234
+ const goToPage = useCallback2((newPage) => {
206
235
  const clampedPage = Math.max(1, Math.min(newPage, totalPages));
207
236
  setPage(clampedPage);
208
237
  }, [totalPages]);
@@ -235,12 +264,21 @@ function useInfiniteQuery(object, options = {}) {
235
264
  const resolvedFields = fields;
236
265
  const resolvedWhere = where;
237
266
  const resolvedSort = orderBy;
267
+ const queryKey = stableKey({
268
+ query,
269
+ where: resolvedWhere,
270
+ fields: resolvedFields,
271
+ orderBy: resolvedSort,
272
+ pageSize
273
+ });
274
+ const handleSuccess = useEventCallback(onSuccess);
275
+ const handleError = useEventCallback(onError);
238
276
  const [pages, setPages] = useState([]);
239
277
  const [isLoading, setIsLoading] = useState(true);
240
278
  const [isFetchingNextPage, setIsFetchingNextPage] = useState(false);
241
279
  const [error, setError] = useState(null);
242
280
  const [hasNextPage, setHasNextPage] = useState(true);
243
- const fetchPage = useCallback(async (skip, isNextPage = false) => {
281
+ const fetchPage = useCallback2(async (skip, isNextPage = false) => {
244
282
  try {
245
283
  if (isNextPage) {
246
284
  setIsFetchingNextPage(true);
@@ -272,27 +310,27 @@ function useInfiniteQuery(object, options = {}) {
272
310
  const fetchedCount = result.records?.length ?? 0;
273
311
  const hasMore = fetchedCount === pageSize;
274
312
  setHasNextPage(hasMore);
275
- onSuccess?.(result);
313
+ handleSuccess(result);
276
314
  } catch (err) {
277
315
  const error2 = err instanceof Error ? err : new Error("Query failed");
278
316
  setError(error2);
279
- onError?.(error2);
317
+ handleError(error2);
280
318
  } finally {
281
319
  setIsLoading(false);
282
320
  setIsFetchingNextPage(false);
283
321
  }
284
- }, [client, object, query, resolvedFields, resolvedWhere, resolvedSort, pageSize, onSuccess, onError]);
285
- useEffect(() => {
322
+ }, [client, object, queryKey, handleSuccess, handleError]);
323
+ useEffect2(() => {
286
324
  if (enabled) {
287
325
  fetchPage(0);
288
326
  }
289
327
  }, [enabled, fetchPage]);
290
- const fetchNextPage = useCallback(async () => {
328
+ const fetchNextPage = useCallback2(async () => {
291
329
  if (!hasNextPage || isFetchingNextPage) return;
292
330
  const nextSkip = pages.length * pageSize;
293
331
  await fetchPage(nextSkip, true);
294
332
  }, [hasNextPage, isFetchingNextPage, pages.length, pageSize, fetchPage]);
295
- const refetch = useCallback(async () => {
333
+ const refetch = useCallback2(async () => {
296
334
  setPages([]);
297
335
  await fetchPage(0);
298
336
  }, [fetchPage]);
@@ -310,7 +348,7 @@ function useInfiniteQuery(object, options = {}) {
310
348
  }
311
349
 
312
350
  // src/metadata-hooks.tsx
313
- import { useState as useState2, useEffect as useEffect2, useCallback as useCallback2 } from "react";
351
+ import { useState as useState2, useEffect as useEffect3, useCallback as useCallback3, useRef as useRef4 } from "react";
314
352
  function useObject(objectName, options = {}) {
315
353
  const client = useClient();
316
354
  const locale = useObjectStackLocale();
@@ -327,7 +365,15 @@ function useObject(objectName, options = {}) {
327
365
  onSuccess,
328
366
  onError
329
367
  } = options;
330
- const fetchMetadata = useCallback2(async () => {
368
+ const dataRef = useRef4(data);
369
+ const etagRef = useRef4(etag);
370
+ useEffect3(() => {
371
+ dataRef.current = data;
372
+ etagRef.current = etag;
373
+ }, [data, etag]);
374
+ const handleSuccess = useEventCallback(onSuccess);
375
+ const handleError = useEventCallback(onError);
376
+ const fetchMetadata = useCallback3(async () => {
331
377
  if (!enabled) return;
332
378
  try {
333
379
  setIsLoading(true);
@@ -335,7 +381,7 @@ function useObject(objectName, options = {}) {
335
381
  setFromCache(false);
336
382
  if (useCache) {
337
383
  const result = await client.meta.getCached(objectName, {
338
- ifNoneMatch: ifNoneMatch || etag,
384
+ ifNoneMatch: ifNoneMatch || etagRef.current,
339
385
  ifModifiedSince
340
386
  });
341
387
  if (result.notModified) {
@@ -346,24 +392,24 @@ function useObject(objectName, options = {}) {
346
392
  setEtag(result.etag.value);
347
393
  }
348
394
  }
349
- onSuccess?.(result.data || data);
395
+ handleSuccess(result.data || dataRef.current);
350
396
  } else {
351
397
  const result = await client.meta.getItem("object", objectName);
352
398
  setData(result);
353
- onSuccess?.(result);
399
+ handleSuccess(result);
354
400
  }
355
401
  } catch (err) {
356
402
  const error2 = err instanceof Error ? err : new Error("Failed to fetch object metadata");
357
403
  setError(error2);
358
- onError?.(error2);
404
+ handleError(error2);
359
405
  } finally {
360
406
  setIsLoading(false);
361
407
  }
362
- }, [client, objectName, locale, enabled, useCache, ifNoneMatch, ifModifiedSince, etag, data, onSuccess, onError]);
363
- useEffect2(() => {
408
+ }, [client, objectName, locale, enabled, useCache, ifNoneMatch, ifModifiedSince, handleSuccess, handleError]);
409
+ useEffect3(() => {
364
410
  fetchMetadata();
365
411
  }, [fetchMetadata]);
366
- const refetch = useCallback2(async () => {
412
+ const refetch = useCallback3(async () => {
367
413
  await fetchMetadata();
368
414
  }, [fetchMetadata]);
369
415
  return {
@@ -382,26 +428,28 @@ function useView(objectName, viewType = "list", options = {}) {
382
428
  const [isLoading, setIsLoading] = useState2(true);
383
429
  const [error, setError] = useState2(null);
384
430
  const { enabled = true, onSuccess, onError } = options;
385
- const fetchView = useCallback2(async () => {
431
+ const handleSuccess = useEventCallback(onSuccess);
432
+ const handleError = useEventCallback(onError);
433
+ const fetchView = useCallback3(async () => {
386
434
  if (!enabled) return;
387
435
  try {
388
436
  setIsLoading(true);
389
437
  setError(null);
390
438
  const result = await client.meta.getView(objectName, viewType);
391
439
  setData(result);
392
- onSuccess?.(result);
440
+ handleSuccess(result);
393
441
  } catch (err) {
394
442
  const error2 = err instanceof Error ? err : new Error("Failed to fetch view configuration");
395
443
  setError(error2);
396
- onError?.(error2);
444
+ handleError(error2);
397
445
  } finally {
398
446
  setIsLoading(false);
399
447
  }
400
- }, [client, objectName, viewType, locale, enabled, onSuccess, onError]);
401
- useEffect2(() => {
448
+ }, [client, objectName, viewType, locale, enabled, handleSuccess, handleError]);
449
+ useEffect3(() => {
402
450
  fetchView();
403
451
  }, [fetchView]);
404
- const refetch = useCallback2(async () => {
452
+ const refetch = useCallback3(async () => {
405
453
  await fetchView();
406
454
  }, [fetchView]);
407
455
  return {
@@ -430,26 +478,29 @@ function useMetadata(fetcher, options = {}) {
430
478
  const [isLoading, setIsLoading] = useState2(true);
431
479
  const [error, setError] = useState2(null);
432
480
  const { enabled = true, onSuccess, onError } = options;
433
- const fetchMetadata = useCallback2(async () => {
481
+ const runFetcher = useEventCallback(fetcher);
482
+ const handleSuccess = useEventCallback(onSuccess);
483
+ const handleError = useEventCallback(onError);
484
+ const fetchMetadata = useCallback3(async () => {
434
485
  if (!enabled) return;
435
486
  try {
436
487
  setIsLoading(true);
437
488
  setError(null);
438
- const result = await fetcher(client);
489
+ const result = await runFetcher(client);
439
490
  setData(result);
440
- onSuccess?.(result);
491
+ handleSuccess(result);
441
492
  } catch (err) {
442
493
  const error2 = err instanceof Error ? err : new Error("Failed to fetch metadata");
443
494
  setError(error2);
444
- onError?.(error2);
495
+ handleError(error2);
445
496
  } finally {
446
497
  setIsLoading(false);
447
498
  }
448
- }, [client, fetcher, locale, enabled, onSuccess, onError]);
449
- useEffect2(() => {
499
+ }, [client, runFetcher, locale, enabled, handleSuccess, handleError]);
500
+ useEffect3(() => {
450
501
  fetchMetadata();
451
502
  }, [fetchMetadata]);
452
- const refetch = useCallback2(async () => {
503
+ const refetch = useCallback3(async () => {
453
504
  await fetchMetadata();
454
505
  }, [fetchMetadata]);
455
506
  return {
@@ -462,11 +513,11 @@ function useMetadata(fetcher, options = {}) {
462
513
  }
463
514
 
464
515
  // src/realtime-hooks.tsx
465
- import { useEffect as useEffect3, useState as useState3, useCallback as useCallback3 } from "react";
516
+ import { useEffect as useEffect4, useState as useState3 } from "react";
466
517
  function useMetadataSubscription(type, options) {
467
518
  const client = useClient();
468
519
  const [event, setEvent] = useState3(null);
469
- useEffect3(() => {
520
+ useEffect4(() => {
470
521
  if (!client) return;
471
522
  const unsubscribe = client.events.subscribeMetadata(
472
523
  type,
@@ -482,7 +533,7 @@ function useMetadataSubscription(type, options) {
482
533
  function useDataSubscription(object, options) {
483
534
  const client = useClient();
484
535
  const [event, setEvent] = useState3(null);
485
- useEffect3(() => {
536
+ useEffect4(() => {
486
537
  if (!client) return;
487
538
  const unsubscribe = client.events.subscribeData(
488
539
  object,
@@ -497,36 +548,61 @@ function useDataSubscription(object, options) {
497
548
  }
498
549
  function useMetadataSubscriptionCallback(type, callback, options) {
499
550
  const client = useClient();
500
- useEffect3(() => {
551
+ const handleEvent = useEventCallback(callback);
552
+ useEffect4(() => {
501
553
  if (!client) return;
502
554
  const unsubscribe = client.events.subscribeMetadata(
503
555
  type,
504
- callback,
556
+ handleEvent,
505
557
  options
506
558
  );
507
559
  return () => {
508
560
  unsubscribe();
509
561
  };
510
- }, [client, type, callback, options?.packageId]);
562
+ }, [client, type, handleEvent, options?.packageId]);
511
563
  }
512
564
  function useDataSubscriptionCallback(object, callback, options) {
513
565
  const client = useClient();
514
- useEffect3(() => {
566
+ const handleEvent = useEventCallback(callback);
567
+ useEffect4(() => {
515
568
  if (!client) return;
516
569
  const unsubscribe = client.events.subscribeData(
517
570
  object,
518
- callback,
571
+ handleEvent,
519
572
  options
520
573
  );
521
574
  return () => {
522
575
  unsubscribe();
523
576
  };
524
- }, [client, object, callback, options?.recordId]);
577
+ }, [client, object, handleEvent, options?.recordId]);
578
+ }
579
+ function useBulkDataSubscription(object) {
580
+ const client = useClient();
581
+ const [event, setEvent] = useState3(null);
582
+ useEffect4(() => {
583
+ if (!client) return;
584
+ const unsubscribe = client.events.subscribeBulkData(object, (e) => setEvent(e));
585
+ return () => {
586
+ unsubscribe();
587
+ };
588
+ }, [client, object]);
589
+ return event;
590
+ }
591
+ function useBulkDataSubscriptionCallback(object, callback) {
592
+ const client = useClient();
593
+ const handleEvent = useEventCallback(callback);
594
+ useEffect4(() => {
595
+ if (!client) return;
596
+ const unsubscribe = client.events.subscribeBulkData(object, handleEvent);
597
+ return () => {
598
+ unsubscribe();
599
+ };
600
+ }, [client, object, handleEvent]);
525
601
  }
526
602
  function useRealtimeConnection() {
527
603
  const client = useClient();
528
604
  const [connected, setConnected] = useState3(true);
529
- useEffect3(() => {
605
+ useEffect4(() => {
530
606
  if (!client) {
531
607
  setConnected(false);
532
608
  return;
@@ -536,10 +612,8 @@ function useRealtimeConnection() {
536
612
  return connected;
537
613
  }
538
614
  function useAutoRefresh(object, refetch, options) {
539
- const handleEvent = useCallback3((_event) => {
540
- refetch();
541
- }, [refetch]);
542
- useDataSubscriptionCallback(object, handleEvent, options);
615
+ useDataSubscriptionCallback(object, (_event) => refetch(), options);
616
+ useBulkDataSubscriptionCallback(object, (_event) => refetch());
543
617
  }
544
618
 
545
619
  // src/index.tsx
@@ -550,6 +624,8 @@ export {
550
624
  ObjectStackLocaleContext,
551
625
  ObjectStackProvider,
552
626
  useAutoRefresh,
627
+ useBulkDataSubscription,
628
+ useBulkDataSubscriptionCallback,
553
629
  useClient,
554
630
  useDataSubscription,
555
631
  useDataSubscriptionCallback,