@dismissible/react-client 2.1.0 → 3.0.0

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/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ # [3.0.0](https://github.com/DismissibleIo/dismissible-react-client/compare/v2.1.0...v3.0.0) (2026-01-18)
2
+
3
+
4
+ ### Features
5
+
6
+ * **batch:** support for the new batch endpoint ([#8](https://github.com/DismissibleIo/dismissible-react-client/issues/8)) ([748fa71](https://github.com/DismissibleIo/dismissible-react-client/commit/748fa71c4d3b1eafed694d06c478bebb8e9d2ea0))
7
+
8
+
9
+ ### BREAKING CHANGES
10
+
11
+ * **batch:** Requires the new batch method on the http client
12
+
1
13
  # [2.1.0](https://github.com/DismissibleIo/dismissible-react-client/compare/v2.0.0...v2.1.0) (2026-01-16)
2
14
 
3
15
 
package/README.md CHANGED
@@ -25,6 +25,7 @@ This component is used with the [Dismissible API Server](https://github.com/Dism
25
25
 
26
26
  - **Easy to use** - Simple component API for dismissible content
27
27
  - **Persistent state** - Dismissal state is saved and restored across sessions when using the [Dismissible API Server](https://github.com/DismissibleIo/dismissible-api)
28
+ - **Automatic request batching** - Multiple items requested in the same render cycle are automatically coalesced into a single API call
28
29
  - **Restore support** - Restore previously dismissed items programmatically
29
30
  - **JWT Authentication** - Built-in support for secure JWT-based authentication
30
31
  - **Custom HTTP Client** - Bring your own HTTP client (axios, ky, etc.) with custom headers, interceptors, and tracking
@@ -550,6 +551,115 @@ function RestorableBanner({ itemId }) {
550
551
 
551
552
  ## Advanced Usage
552
553
 
554
+ ### Automatic Request Batching
555
+
556
+ The library automatically batches multiple dismissible item requests into a single API call, dramatically reducing network overhead when rendering pages with many dismissible components.
557
+
558
+ #### How It Works
559
+
560
+ Under the hood, Dismissible uses a `BatchScheduler` that implements [DataLoader](https://github.com/graphql/dataloader)-style request coalescing:
561
+
562
+ 1. **Request Collection**: When multiple `<Dismissible>` components or `useDismissibleItem` hooks mount during the same render cycle, each request is queued rather than fired immediately.
563
+
564
+ 2. **Microtask Scheduling**: The scheduler uses `queueMicrotask()` to defer execution until after all synchronous code in the current JavaScript tick completes.
565
+
566
+ 3. **Batch Execution**: All queued requests are combined into a single batch API call (up to 50 items per batch, with automatic splitting for larger sets).
567
+
568
+ 4. **Result Distribution**: When the API responds, results are distributed back to each waiting component.
569
+
570
+ ```
571
+ ┌─────────────────────────────────────────────────────────────────┐
572
+ │ Same JavaScript Tick │
573
+ ├─────────────────────────────────────────────────────────────────┤
574
+ │ Component A Component B Component C │
575
+ │ requests "banner" requests "modal" requests "tooltip" │
576
+ │ │ │ │ │
577
+ │ └────────────────────┼────────────────────┘ │
578
+ │ ▼ │
579
+ │ ┌───────────────┐ │
580
+ │ │ BatchScheduler│ │
581
+ │ │ Queue │ │
582
+ │ └───────┬───────┘ │
583
+ │ │ │
584
+ │ queueMicrotask │
585
+ └────────────────────────────┼────────────────────────────────────┘
586
+
587
+ ┌─────────────────────────┐
588
+ │ Single API Call │
589
+ │ POST /v1/users/{id}/ │
590
+ │ items/batch │
591
+ │ ["banner", "modal", │
592
+ │ "tooltip"] │
593
+ └─────────────────────────┘
594
+ ```
595
+
596
+ #### Example: Dashboard with Multiple Dismissibles
597
+
598
+ ```tsx
599
+ // Without batching: 5 separate API calls
600
+ // With batching: 1 single API call containing all 5 item IDs
601
+
602
+ function Dashboard() {
603
+ return (
604
+ <div>
605
+ <Dismissible itemId="welcome-banner">
606
+ <WelcomeBanner />
607
+ </Dismissible>
608
+
609
+ <Dismissible itemId="feature-announcement">
610
+ <FeatureAnnouncement />
611
+ </Dismissible>
612
+
613
+ <Dismissible itemId="survey-prompt">
614
+ <SurveyPrompt />
615
+ </Dismissible>
616
+
617
+ <Dismissible itemId="upgrade-notice">
618
+ <UpgradeNotice />
619
+ </Dismissible>
620
+
621
+ <Dismissible itemId="maintenance-alert">
622
+ <MaintenanceAlert />
623
+ </Dismissible>
624
+ </div>
625
+ );
626
+ }
627
+ ```
628
+
629
+ #### Built-in Optimizations
630
+
631
+ The `BatchScheduler` includes several optimizations:
632
+
633
+ - **Request Deduplication**: If the same `itemId` is requested multiple times in the same tick, only one request is made and the result is shared.
634
+ - **In-Memory Caching**: Previously fetched items are cached in memory to avoid redundant API calls.
635
+ - **Cache Priming**: Items loaded from localStorage are automatically primed in the batch cache.
636
+ - **Cache Sync**: When items are dismissed or restored, the batch cache is updated to ensure consistency.
637
+
638
+ #### Using the Hook with Batching
639
+
640
+ The batching is completely transparent when using the `useDismissibleItem` hook:
641
+
642
+ ```tsx
643
+ function NotificationCenter() {
644
+ // All three hooks will batch their requests into a single API call
645
+ const notification1 = useDismissibleItem('notification-1');
646
+ const notification2 = useDismissibleItem('notification-2');
647
+ const notification3 = useDismissibleItem('notification-3');
648
+
649
+ // Rendering logic...
650
+ }
651
+ ```
652
+
653
+ #### Performance Impact
654
+
655
+ | Scenario | Without Batching | With Batching |
656
+ |----------|------------------|---------------|
657
+ | 5 dismissible items | 5 HTTP requests | 1 HTTP request |
658
+ | 20 dismissible items | 20 HTTP requests | 1 HTTP request |
659
+ | 100 dismissible items | 100 HTTP requests | 2 HTTP requests* |
660
+
661
+ \* *Batches are automatically split at 50 items to respect API limits*
662
+
553
663
  ### Custom HTTP Client
554
664
 
555
665
  By default, Dismissible uses a built-in HTTP client powered by `openapi-fetch`. However, you can provide your own HTTP client implementation by passing a `client` prop to the `DismissibleProvider`. This is useful when you need:
@@ -576,6 +686,15 @@ interface DismissibleClient {
576
686
  signal?: AbortSignal;
577
687
  }) => Promise<DismissibleItem>;
578
688
 
689
+ // Required for automatic batching - fetches multiple items in one API call
690
+ batchGetOrCreate: (params: {
691
+ userId: string;
692
+ itemIds: string[]; // Array of item IDs (max 50)
693
+ baseUrl: string;
694
+ authHeaders: { Authorization?: string };
695
+ signal?: AbortSignal;
696
+ }) => Promise<DismissibleItem[]>;
697
+
579
698
  dismiss: (params: {
580
699
  userId: string;
581
700
  itemId: string;
@@ -592,6 +711,8 @@ interface DismissibleClient {
592
711
  }
593
712
  ```
594
713
 
714
+ > **Note**: The `batchGetOrCreate` method is essential for the automatic request batching feature. When multiple components request items in the same render cycle, this method is called instead of multiple `getOrCreate` calls.
715
+
595
716
  #### Example: Custom Client with Axios
596
717
 
597
718
  ```tsx
@@ -615,6 +736,22 @@ const axiosClient: DismissibleClient = {
615
736
  return response.data.data;
616
737
  },
617
738
 
739
+ // Batch endpoint for automatic request coalescing
740
+ batchGetOrCreate: async ({ userId, itemIds, baseUrl, authHeaders, signal }) => {
741
+ const response = await axios.post(
742
+ `${baseUrl}/v1/users/${userId}/items/batch`,
743
+ { itemIds },
744
+ {
745
+ headers: {
746
+ ...authHeaders,
747
+ 'X-Correlation-ID': uuid(),
748
+ },
749
+ signal,
750
+ }
751
+ );
752
+ return response.data.data;
753
+ },
754
+
618
755
  dismiss: async ({ userId, itemId, baseUrl, authHeaders }) => {
619
756
  const response = await axios.delete(
620
757
  `${baseUrl}/v1/users/${userId}/items/${itemId}`,
@@ -685,6 +822,33 @@ const loggingClient: DismissibleClient = {
685
822
  return data.data;
686
823
  },
687
824
 
825
+ batchGetOrCreate: async ({ userId, itemIds, baseUrl, authHeaders, signal }) => {
826
+ console.log(`[Dismissible] Batch fetching ${itemIds.length} items for user: ${userId}`);
827
+ const startTime = performance.now();
828
+
829
+ const response = await fetch(
830
+ `${baseUrl}/v1/users/${userId}/items/batch`,
831
+ {
832
+ method: 'POST',
833
+ headers: {
834
+ ...authHeaders,
835
+ 'Content-Type': 'application/json',
836
+ },
837
+ body: JSON.stringify({ itemIds }),
838
+ signal,
839
+ }
840
+ );
841
+
842
+ const data = await response.json();
843
+ console.log(`[Dismissible] Batch fetched ${itemIds.length} items in ${performance.now() - startTime}ms`);
844
+
845
+ if (!response.ok) {
846
+ throw new Error(data.message || 'Failed to batch fetch dismissible items');
847
+ }
848
+
849
+ return data.data;
850
+ },
851
+
688
852
  dismiss: async ({ userId, itemId, baseUrl, authHeaders }) => {
689
853
  console.log(`[Dismissible] Dismissing item: ${itemId}`);
690
854
 
@@ -776,6 +940,20 @@ const retryClient: DismissibleClient = {
776
940
  return data.data;
777
941
  },
778
942
 
943
+ batchGetOrCreate: async ({ userId, itemIds, baseUrl, authHeaders, signal }) => {
944
+ const response = await fetchWithRetry(
945
+ `${baseUrl}/v1/users/${userId}/items/batch`,
946
+ {
947
+ method: 'POST',
948
+ headers: { ...authHeaders, 'Content-Type': 'application/json' },
949
+ body: JSON.stringify({ itemIds }),
950
+ signal,
951
+ }
952
+ );
953
+ const data = await response.json();
954
+ return data.data;
955
+ },
956
+
779
957
  dismiss: async ({ userId, itemId, baseUrl, authHeaders }) => {
780
958
  const response = await fetchWithRetry(
781
959
  `${baseUrl}/v1/users/${userId}/items/${itemId}`,