Skip to content

Conversation

@ChaserZ98
Copy link
Contributor

@ChaserZ98 ChaserZ98 commented Aug 17, 2025

Related Issue #5559
Related PR #5602 #5590

📝 Description

This PR adopts the multi toaster design from react-hot-toast and adds support for multiple ToastProvider. It might provide an alternative solution to the above issues and PRs.

⛳️ Current behavior (updates)

All the ToastProvider instance shares the same ToastQueue. If there are multiple ToastProvider instances, call addToast once would cause all the ToastProvider instance to render the same toast, and hence create duplicate toasts.

🚀 New behavior

Each ToastProvider instance has their own ToastQueue and an unique ID field toasterId (default to heroui) is used to distinguish each of them. addToast and closeToast API also receives this toasterId field to control the add and close behavior of the corresponding ToastProvider.

💣 Is this a breaking change (Yes/No):

Only a bit change on params, so kind of a no.

📝 Additional Information

Please see the storybook Multi Toaster example for demo.

Summary by CodeRabbit

  • New Features

    • Support multiple ToastProviders with unique IDs and independent toast queues; toasts can be targeted, closed, and cleared per provider via a toasterId prop.
  • Documentation

    • Added a Storybook/demo showing two providers with separate queues and controls.
  • Tests

    • Added a unit test verifying multiple ToastProvider instances operate independently and scope placement/content.
  • Chores

    • Added a changeset for a patch release of the toast package.

@ChaserZ98 ChaserZ98 requested a review from jrgarciadev as a code owner August 17, 2025 18:03
@changeset-bot
Copy link

changeset-bot bot commented Aug 17, 2025

🦋 Changeset detected

Latest commit: c6ab38f

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@heroui/toast Patch
@heroui/react Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel
Copy link

vercel bot commented Aug 17, 2025

@ChaserZ98 is attempting to deploy a commit to the HeroUI Inc Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 17, 2025

Walkthrough

Adds support for multiple ToastProvider instances by introducing per-toaster queues keyed by toasterId, updating toast API signatures and ToastProvider to accept toasterId, and adding stories, tests, docs, and a changeset for the feature.

Changes

Cohort / File(s) Summary
Toast provider & queue routing
packages/components/toast/src/toast-provider.tsx
Replace single global queue with globalToastQueues keyed by toasterId. Update getToastQueue (overloads), addToast, closeToast, closeAll to accept/use toasterId. ToastProvider gains toasterId prop (defaults to defaultToasterId) and binds to per-id queue.
Toast props surface
packages/components/toast/src/use-toast.ts
Add public toasterId?: string to ToastProps to target a specific ToastProvider.
Stories / examples
packages/components/toast/stories/toast.stories.tsx, apps/docs/content/components/toast/multiple-ToastProvider.raw.jsx, apps/docs/content/components/toast/multiple-ToastProvider.ts, apps/docs/content/docs/components/toast.mdx, apps/docs/content/components/toast/index.ts
Add a MultiToaster story and docs example demonstrating two ToastProvider instances with distinct toasterIds and buttons that call addToast({ toasterId, ... }). Export the example in docs content and include it in toastContent.
Tests
packages/components/toast/__tests__/toast.test.tsx
Add unit test "should work with multiple ToastProvider" validating isolation and correct routing/placement for two providers keyed by toasterId.
Release metadata
.changeset/great-keys-admire.md
Add changeset for @heroui/toast patch release describing support for multiple ToastProvider instances via unique IDs.

Sequence Diagram(s)

sequenceDiagram
  participant UI as Caller (e.g., Button)
  participant API as addToast / closeToast / closeAll
  participant Map as globalToastQueues
  participant TP as ToastProvider (toasterId)

  UI->>API: addToast({ toasterId, ... })
  API->>Map: get/create queue for toasterId
  Map-->>API: ToastQueue
  API-->>TP: enqueue toast in matching ToastQueue
  TP->>TP: render region for its toasterId

  UI->>API: closeToast({ key, toasterId })
  API->>Map: get queue for toasterId
  API-->>TP: close toast by key

  UI->>API: closeAll(toasterId)
  API->>Map: get queue for toasterId
  API-->>TP: close all visible toasts
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

👀 Status: To Review

Suggested reviewers

  • jrgarciadev
  • wingkwong
  • macci001

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@pkg-pr-new
Copy link

pkg-pr-new bot commented Aug 17, 2025

Open in StackBlitz

@heroui/accordion

npm i https://pkg.pr.new/@heroui/accordion@5606

@heroui/alert

npm i https://pkg.pr.new/@heroui/alert@5606

@heroui/autocomplete

npm i https://pkg.pr.new/@heroui/autocomplete@5606

@heroui/avatar

npm i https://pkg.pr.new/@heroui/avatar@5606

@heroui/badge

npm i https://pkg.pr.new/@heroui/badge@5606

@heroui/breadcrumbs

npm i https://pkg.pr.new/@heroui/breadcrumbs@5606

@heroui/button

npm i https://pkg.pr.new/@heroui/button@5606

@heroui/calendar

npm i https://pkg.pr.new/@heroui/calendar@5606

@heroui/card

npm i https://pkg.pr.new/@heroui/card@5606

@heroui/checkbox

npm i https://pkg.pr.new/@heroui/checkbox@5606

@heroui/chip

npm i https://pkg.pr.new/@heroui/chip@5606

@heroui/code

npm i https://pkg.pr.new/@heroui/code@5606

@heroui/date-input

npm i https://pkg.pr.new/@heroui/date-input@5606

@heroui/date-picker

npm i https://pkg.pr.new/@heroui/date-picker@5606

@heroui/divider

npm i https://pkg.pr.new/@heroui/divider@5606

@heroui/drawer

npm i https://pkg.pr.new/@heroui/drawer@5606

@heroui/dropdown

npm i https://pkg.pr.new/@heroui/dropdown@5606

@heroui/form

npm i https://pkg.pr.new/@heroui/form@5606

@heroui/image

npm i https://pkg.pr.new/@heroui/image@5606

@heroui/input

npm i https://pkg.pr.new/@heroui/input@5606

@heroui/input-otp

npm i https://pkg.pr.new/@heroui/input-otp@5606

@heroui/kbd

npm i https://pkg.pr.new/@heroui/kbd@5606

@heroui/link

npm i https://pkg.pr.new/@heroui/link@5606

@heroui/listbox

npm i https://pkg.pr.new/@heroui/listbox@5606

@heroui/menu

npm i https://pkg.pr.new/@heroui/menu@5606

@heroui/modal

npm i https://pkg.pr.new/@heroui/modal@5606

@heroui/navbar

npm i https://pkg.pr.new/@heroui/navbar@5606

@heroui/number-input

npm i https://pkg.pr.new/@heroui/number-input@5606

@heroui/pagination

npm i https://pkg.pr.new/@heroui/pagination@5606

@heroui/popover

npm i https://pkg.pr.new/@heroui/popover@5606

@heroui/progress

npm i https://pkg.pr.new/@heroui/progress@5606

@heroui/radio

npm i https://pkg.pr.new/@heroui/radio@5606

@heroui/ripple

npm i https://pkg.pr.new/@heroui/ripple@5606

@heroui/scroll-shadow

npm i https://pkg.pr.new/@heroui/scroll-shadow@5606

@heroui/select

npm i https://pkg.pr.new/@heroui/select@5606

@heroui/skeleton

npm i https://pkg.pr.new/@heroui/skeleton@5606

@heroui/slider

npm i https://pkg.pr.new/@heroui/slider@5606

@heroui/snippet

npm i https://pkg.pr.new/@heroui/snippet@5606

@heroui/spacer

npm i https://pkg.pr.new/@heroui/spacer@5606

@heroui/spinner

npm i https://pkg.pr.new/@heroui/spinner@5606

@heroui/switch

npm i https://pkg.pr.new/@heroui/switch@5606

@heroui/table

npm i https://pkg.pr.new/@heroui/table@5606

@heroui/tabs

npm i https://pkg.pr.new/@heroui/tabs@5606

@heroui/toast

npm i https://pkg.pr.new/@heroui/toast@5606

@heroui/tooltip

npm i https://pkg.pr.new/@heroui/tooltip@5606

@heroui/user

npm i https://pkg.pr.new/@heroui/user@5606

@heroui/react

npm i https://pkg.pr.new/@heroui/react@5606

@heroui/system

npm i https://pkg.pr.new/@heroui/system@5606

@heroui/system-rsc

npm i https://pkg.pr.new/@heroui/system-rsc@5606

@heroui/theme

npm i https://pkg.pr.new/@heroui/theme@5606

@heroui/use-aria-accordion

npm i https://pkg.pr.new/@heroui/use-aria-accordion@5606

@heroui/use-aria-accordion-item

npm i https://pkg.pr.new/@heroui/use-aria-accordion-item@5606

@heroui/use-aria-button

npm i https://pkg.pr.new/@heroui/use-aria-button@5606

@heroui/use-aria-link

npm i https://pkg.pr.new/@heroui/use-aria-link@5606

@heroui/use-aria-modal-overlay

npm i https://pkg.pr.new/@heroui/use-aria-modal-overlay@5606

@heroui/use-aria-multiselect

npm i https://pkg.pr.new/@heroui/use-aria-multiselect@5606

@heroui/use-aria-overlay

npm i https://pkg.pr.new/@heroui/use-aria-overlay@5606

@heroui/use-callback-ref

npm i https://pkg.pr.new/@heroui/use-callback-ref@5606

@heroui/use-clipboard

npm i https://pkg.pr.new/@heroui/use-clipboard@5606

@heroui/use-data-scroll-overflow

npm i https://pkg.pr.new/@heroui/use-data-scroll-overflow@5606

@heroui/use-disclosure

npm i https://pkg.pr.new/@heroui/use-disclosure@5606

@heroui/use-draggable

npm i https://pkg.pr.new/@heroui/use-draggable@5606

@heroui/use-form-reset

npm i https://pkg.pr.new/@heroui/use-form-reset@5606

@heroui/use-image

npm i https://pkg.pr.new/@heroui/use-image@5606

@heroui/use-infinite-scroll

npm i https://pkg.pr.new/@heroui/use-infinite-scroll@5606

@heroui/use-intersection-observer

npm i https://pkg.pr.new/@heroui/use-intersection-observer@5606

@heroui/use-is-mobile

npm i https://pkg.pr.new/@heroui/use-is-mobile@5606

@heroui/use-is-mounted

npm i https://pkg.pr.new/@heroui/use-is-mounted@5606

@heroui/use-measure

npm i https://pkg.pr.new/@heroui/use-measure@5606

@heroui/use-pagination

npm i https://pkg.pr.new/@heroui/use-pagination@5606

@heroui/use-real-shape

npm i https://pkg.pr.new/@heroui/use-real-shape@5606

@heroui/use-ref-state

npm i https://pkg.pr.new/@heroui/use-ref-state@5606

@heroui/use-resize

npm i https://pkg.pr.new/@heroui/use-resize@5606

@heroui/use-safe-layout-effect

npm i https://pkg.pr.new/@heroui/use-safe-layout-effect@5606

@heroui/use-scroll-position

npm i https://pkg.pr.new/@heroui/use-scroll-position@5606

@heroui/use-ssr

npm i https://pkg.pr.new/@heroui/use-ssr@5606

@heroui/use-theme

npm i https://pkg.pr.new/@heroui/use-theme@5606

@heroui/use-update-effect

npm i https://pkg.pr.new/@heroui/use-update-effect@5606

@heroui/use-viewport-size

npm i https://pkg.pr.new/@heroui/use-viewport-size@5606

@heroui/aria-utils

npm i https://pkg.pr.new/@heroui/aria-utils@5606

@heroui/dom-animation

npm i https://pkg.pr.new/@heroui/dom-animation@5606

@heroui/framer-utils

npm i https://pkg.pr.new/@heroui/framer-utils@5606

@heroui/react-rsc-utils

npm i https://pkg.pr.new/@heroui/react-rsc-utils@5606

@heroui/react-utils

npm i https://pkg.pr.new/@heroui/react-utils@5606

@heroui/shared-icons

npm i https://pkg.pr.new/@heroui/shared-icons@5606

@heroui/shared-utils

npm i https://pkg.pr.new/@heroui/shared-utils@5606

@heroui/stories-utils

npm i https://pkg.pr.new/@heroui/stories-utils@5606

@heroui/test-utils

npm i https://pkg.pr.new/@heroui/test-utils@5606

commit: c6ab38f

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🔭 Outside diff range comments (1)
.changeset/great-keys-admire.md (1)

1-6: CloseToast API breaking change – adjust version or add compatibility overload

The closeToast signature was updated from closeToast(key: string) to closeToast({ key, toasterId }), which will break existing call sites and documentation. You must either:

  • Add a backward-compatible overload:

    export function closeToast(key: string): void
    export function closeToast(opts: { key: string; toasterId?: string }): void

    internally delegating to the new signature, and keep this as a patch release.

  • Or bump this changeset to a minor (or major) version and clearly note the breaking change.

Also update documentation to cover:

  • The default toasterId value ("heroui")
  • That addToast, closeToast, and closeAll accept an optional toasterId parameter
  • What happens when you target a non-existent toasterId

Affected call sites:

  • apps/docs/content/components/toast/close.raw.jsx:
    closeToast(toastKey[toastKey.length – 1])
  • packages/components/toast/stories/toast.stories.tsx:
    closeToast(toastKey[toastKey.length – 1])
  • apps/docs/content/blog/v2.8.0.mdx: update former closeToast(key: string) description to reflect the new API and version bump
🧹 Nitpick comments (4)
packages/components/toast/src/use-toast.ts (1)

113-117: Docstring grammar + clarity nits for toasterId

Minor copy fix and clarity improvement.

Apply:

   /**
-   * The id of the ToastProvider. The decides which ToastProvider will handle the toast.
+   * The id of the ToastProvider. Determines which ToastProvider will handle this toast.
    * @default "heroui"
    */
   toasterId?: string;
packages/components/toast/stories/toast.stories.tsx (2)

428-469: Nice MultiToaster demo; add cleanup to prevent cross-story leftovers

With per-id queues, the top-level decorator’s closeAll() only clears the default “heroui” queue. MultiToaster uses “left” and “right”, so toasts can persist across story changes. Either:

  • extend closeAll to support closing all queues (see provider comment), then call closeAll("all") in the decorator, or
  • add a local cleanup here to clear both queues on unmount.

Example local cleanup:

useEffect(() => {
  return () => {
    closeAll("left");
    closeAll("right");
  };
}, []);

575-580: Export of MultiToaster story: consider adding note in story description

To aid discoverability, consider adding a short docs block explaining that toasterId routes toasts to specific providers and that the default id is “heroui”.

packages/components/toast/src/toast-provider.tsx (1)

87-97: closeAll should optionally support clearing all toasters; avoid using map for side-effects

Two suggestions:

  • Allow closeAll("all") to clear every queue (handy for Storybook cleanup).
  • Use forEach instead of map for side effects.
-export const closeAll = (toasterId = defaultToasterId) => {
-  if (!globalToastQueues[toasterId]) {
-    return;
-  }
-
-  const keys = globalToastQueues[toasterId].visibleToasts.map((toast) => toast.key);
-
-  keys.map((key) => {
-    globalToastQueues[toasterId]?.close(key);
-  });
-};
+export const closeAll = (toasterId: string | "all" = defaultToasterId) => {
+  const closeFor = (id: string) => {
+    const queue = globalToastQueues[id];
+    if (!queue) return;
+    queue.visibleToasts.forEach((t) => queue.close(t.key));
+  };
+  if (toasterId === "all") {
+    Object.keys(globalToastQueues).forEach(closeFor);
+    return;
+  }
+  closeFor(toasterId);
+};

If you adopt "all", update Storybook decorators to call closeAll("all").

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 8e51a85 and 1dad8f5.

📒 Files selected for processing (4)
  • .changeset/great-keys-admire.md (1 hunks)
  • packages/components/toast/src/toast-provider.tsx (4 hunks)
  • packages/components/toast/src/use-toast.ts (1 hunks)
  • packages/components/toast/stories/toast.stories.tsx (2 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (2)
packages/components/toast/stories/toast.stories.tsx (1)
packages/components/toast/src/toast-provider.tsx (2)
  • ToastProvider (36-64)
  • addToast (66-72)
packages/components/toast/src/toast-provider.tsx (3)
packages/components/toast/src/use-toast.ts (1)
  • ToastProps (27-118)
packages/components/toast/src/toast.tsx (1)
  • ToastProps (20-20)
packages/components/toast/src/index.ts (5)
  • ToastProps (5-5)
  • getToastQueue (9-9)
  • addToast (9-9)
  • closeToast (9-9)
  • closeAll (9-9)
🔇 Additional comments (1)
packages/components/toast/stories/toast.stories.tsx (1)

428-469: Targeted toasts look good and demonstrate the API clearly

Two providers with distinct toasterIds and explicit addToast({toasterId}) calls make the intended behavior obvious. This is a solid example.

Comment on lines +66 to 72
export const addToast = ({toasterId = defaultToasterId, ...props}: ToastProps & ToastOptions) => {
if (!globalToastQueues[toasterId]) {
return null;
}

return globalToastQueue.add(props);
return globalToastQueues[toasterId].add(props);
};
Copy link
Contributor

@coderabbitai coderabbitai bot Aug 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Dropping toasts when queue doesn’t exist is surprising; lazily create instead

Returning null if the toasterId queue isn’t initialized causes lost toasts when addToast is called before the provider mounts. Prefer lazy creation for resilience.

-export const addToast = ({toasterId = defaultToasterId, ...props}: ToastProps & ToastOptions) => {
-  if (!globalToastQueues[toasterId]) {
-    return null;
-  }
-
-  return globalToastQueues[toasterId].add(props);
-};
+export const addToast = ({toasterId = defaultToasterId, ...props}: ToastProps & ToastOptions) => {
+  if (!globalToastQueues[toasterId]) {
+    getToastQueue(toasterId);
+  }
+  return globalToastQueues[toasterId].add(props);
+};

If you intentionally want to drop toasts for non-existent providers, at least log a dev warning to aid debugging.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In packages/components/toast/src/toast-provider.tsx around lines 66 to 72, the
current addToast returns null when globalToastQueues[toasterId] is missing which
drops toasts; change it to lazily create the queue instead of returning null: if
the queue for toasterId doesn't exist, initialize it (using the same queue
creation/registration logic the provider uses) and then call its add method; if
you prefer to keep dropping behavior, at minimum emit a dev-only warning
indicating the toasterId is missing. Ensure the new initialization uses the
existing factory/registry so behavior matches mounted providers.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the original logic of addToast. I doubt if I should switch to this. @wingkwong Can you take a look here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Copy link
Member

@wingkwong wingkwong left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. please add tests
  2. please update docs
  3. please check coderabbitai's comments once

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (3)
packages/components/toast/src/toast-provider.tsx (3)

78-90: closeToast overloads: backward-compatible and solid

Accepting both closeToast(key: string) and closeToast({key, toasterId?}) maintains compatibility while enabling multi-toaster targeting. Guarding when the queue is missing is appropriate.


70-76: addToast currently drops toasts and changes return type; lazily create the queue to preserve API

Returning null when the queue is missing both drops toasts invoked before provider mount and changes the return type to string | null (breaking for consumers expecting a string). Prefer lazy creation to preserve behavior and typings.

Apply:

-export const addToast = ({toasterId = defaultToasterId, ...props}: ToastProps & ToastOptions) => {
-  if (!globalToastQueues[toasterId]) {
-    return null;
-  }
-
-  return globalToastQueues[toasterId].add(props);
-};
+export const addToast = (
+  {toasterId = defaultToasterId, ...props}: ToastProps & ToastOptions
+): string => {
+  if (!globalToastQueues[toasterId]) {
+    getToastQueue(toasterId);
+  }
+  return globalToastQueues[toasterId].add(props);
+};

If you intentionally want to drop toasts when a provider isn’t mounted, at least document the behavior and consider emitting a dev-only warning to aid debugging.


26-38: Restore string overload for backward compatibility

Confirmed that getToastQueue is re-exported in
packages/components/toast/src/index.ts (line 9)
and there are no internal call sites using the string-only signature. To avoid breaking existing consumers, add back the string overload:

Apply:

 export function getToastQueue(): ToastQueue<ToastProps>;
-export function getToastQueue(args: {toasterId: string}): ToastQueue<ToastProps>;
+export function getToastQueue(toasterId: string): ToastQueue<ToastProps>;
+export function getToastQueue(args: {toasterId: string}): ToastQueue<ToastProps>;
 export function getToastQueue(args?: string | {toasterId: string}) {
-  const toasterId = args?.toasterId || defaultToasterId;
+  const toasterId =
+    typeof args === "string" ? args : args?.toasterId || defaultToasterId;
   if (!globalToastQueues[toasterId]) {
     globalToastQueues[toasterId] = new ToastQueue({
       maxVisibleToasts: Infinity,
     });
   }

   return globalToastQueues[toasterId];
 }
🧹 Nitpick comments (3)
packages/components/toast/src/toast-provider.tsx (3)

13-15: Good fix on queue typing; minor nit: prefer const over let

Switching to a non-nullable Record removes the nullable leak. Consider making the map binding immutable since you only mutate its properties.

- let globalToastQueues: Record<string, ToastQueue<ToastProps>> = {};
+ const globalToastQueues: Record<string, ToastQueue<ToastProps>> = {};

40-50: Provider lifecycle cleanup to avoid stale queues (optional)

If a ToastProvider unmounts, its queue remains in the global registry indefinitely. Consider deleting the queue on unmount to prevent leaks and stale state.

Within the component, right after obtaining toastQueue, add:

// Add import at top of file:
// import {useEffect} from "react";

useEffect(() => {
  return () => {
    delete globalToastQueues[toasterId];
  };
}, [toasterId]);

Note: Ensure you don’t have multiple providers sharing the same toasterId. If you do, you may need reference counting instead of unconditional delete.


92-102: Use forEach instead of map for side effects

map is used only for side effects and creates an unused array. Switch to forEach (or a simple for...of) for clarity and micro-optimization.

-  const keys = globalToastQueues[toasterId].visibleToasts.map((toast) => toast.key);
-
-  keys.map((key) => {
-    globalToastQueues[toasterId]?.close(key);
-  });
+  const keys = globalToastQueues[toasterId].visibleToasts.map((toast) => toast.key);
+  keys.forEach((key) => {
+    globalToastQueues[toasterId]?.close(key);
+  });
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 1dad8f5 and 64f5fcb.

📒 Files selected for processing (1)
  • packages/components/toast/src/toast-provider.tsx (3 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
packages/components/toast/src/toast-provider.tsx (3)
packages/components/toast/src/use-toast.ts (1)
  • ToastProps (27-118)
packages/components/toast/src/toast.tsx (1)
  • ToastProps (20-20)
packages/components/toast/src/index.ts (6)
  • ToastProps (5-5)
  • getToastQueue (9-9)
  • ToastProvider (13-13)
  • addToast (9-9)
  • closeToast (9-9)
  • closeAll (9-9)
🔇 Additional comments (2)
packages/components/toast/src/toast-provider.tsx (2)

23-23: Prop addition looks good

Adding toasterId?: string to the provider props cleanly exposes the multi-toaster capability with a sensible default handled elsewhere.


40-50: Confirm maxVisibleToasts semantics: queue is created with Infinity

getToastQueue always constructs the queue with maxVisibleToasts: Infinity, while the provider accepts maxVisibleToasts (default 3). If ToastQueue (from @react-stately/toast) enforces visibility at the queue level (typical), the Region-level prop may not cap visibility. Verify whether the Region also enforces the cap, or consider initializing/updating the queue with the provider’s value.

If needed, one approach is to allow passing options into getToastQueue upon first creation so the provider can set the cap for its toasterId.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (4)
packages/components/toast/__tests__/toast.test.tsx (4)

221-238: Make assertions resilient to async rendering and DOM order

Relying on index-based regions from screen.getAllByRole("region") can be brittle:

  • Rendering of regions is conditional and may be async due to state updates/animation.
  • DOM order could change in the future (e.g., portals, layout changes), making index-based checks flaky.

Prefer awaiting the regions, then selecting by data-placement, and use text-based assertions for clarity.

Apply this diff:

-    await user.click(left_button);
-    await user.click(right_button);
-    const region = screen.getAllByRole("region");
+    await user.click(left_button);
+    await user.click(right_button);
+    const regions = await screen.findAllByRole("region");
+    expect(regions).toHaveLength(2);
+    const leftRegion = regions.find((r) => r.getAttribute("data-placement") === "bottom-left")!;
+    const rightRegion = regions.find((r) => r.getAttribute("data-placement") === "bottom-right")!;
@@
-    // check for left ToastProvider
-    expect(region[0]).toHaveAttribute("data-placement", "bottom-left");
-    expect(region[0]).toContainHTML(left_title);
-    expect(region[0]).toContainHTML(left_description);
-    expect(region[0]).not.toContainHTML(right_title);
-    expect(region[0]).not.toContainHTML(right_description);
+    // check for left ToastProvider
+    expect(leftRegion).toHaveAttribute("data-placement", "bottom-left");
+    expect(leftRegion).toHaveTextContent(left_title);
+    expect(leftRegion).toHaveTextContent(left_description);
+    expect(leftRegion).not.toHaveTextContent(right_title);
+    expect(leftRegion).not.toHaveTextContent(right_description);
@@
-    // check for right ToastProvider
-    expect(region[1]).toHaveAttribute("data-placement", "bottom-right");
-    expect(region[1]).toContainHTML(right_title);
-    expect(region[1]).toContainHTML(right_description);
-    expect(region[1]).not.toContainHTML(left_title);
-    expect(region[1]).not.toContainHTML(left_description);
+    // check for right ToastProvider
+    expect(rightRegion).toHaveAttribute("data-placement", "bottom-right");
+    expect(rightRegion).toHaveTextContent(right_title);
+    expect(rightRegion).toHaveTextContent(right_description);
+    expect(rightRegion).not.toHaveTextContent(left_title);
+    expect(rightRegion).not.toHaveTextContent(left_description);

179-186: Use camelCase for local identifiers in TS tests

Local constants in this test use snake_case, which is inconsistent with the rest of the file and common TS/JS conventions. Renaming improves readability and consistency.

Apply this diff:

-    const left_toaster_id = "left";
-    const left_title = "Left Toast Title";
-    const left_description = "Left Toast Description";
+    const leftToasterId = "left";
+    const leftTitle = "Left Toast Title";
+    const leftDescription = "Left Toast Description";
@@
-    const right_toaster_id = "right";
-    const right_title = "Right Toast Title";
-    const right_description = "Right Toast Description";
+    const rightToasterId = "right";
+    const rightTitle = "Right Toast Title";
+    const rightDescription = "Right Toast Description";
@@
-        <ToastProvider placement="bottom-left" toasterId={left_toaster_id} />
-        <ToastProvider placement="bottom-right" toasterId={right_toaster_id} />
+        <ToastProvider placement="bottom-left" toasterId={leftToasterId} />
+        <ToastProvider placement="bottom-right" toasterId={rightToasterId} />
@@
-            addToast({
-              title: left_title,
-              description: left_description,
-              toasterId: left_toaster_id,
-            });
+            addToast({
+              title: leftTitle,
+              description: leftDescription,
+              toasterId: leftToasterId,
+            });
@@
-            addToast({
-              title: right_title,
-              description: right_description,
-              toasterId: right_toaster_id,
-            });
+            addToast({
+              title: rightTitle,
+              description: rightDescription,
+              toasterId: rightToasterId,
+            });
@@
-    const left_button = wrapper.getByTestId("left-button");
-    const right_button = wrapper.getByTestId("right-button");
+    const left_button = wrapper.getByTestId("left-button");
+    const right_button = wrapper.getByTestId("right-button");
@@
-    expect(region[0]).toContainHTML(left_title);
-    expect(region[0]).toContainHTML(left_description);
-    expect(region[0]).not.toContainHTML(right_title);
-    expect(region[0]).not.toContainHTML(right_description);
+    expect(region[0]).toContainHTML(leftTitle);
+    expect(region[0]).toContainHTML(leftDescription);
+    expect(region[0]).not.toContainHTML(rightTitle);
+    expect(region[0]).not.toContainHTML(rightDescription);
@@
-    expect(region[1]).toContainHTML(right_title);
-    expect(region[1]).toContainHTML(right_description);
-    expect(region[1]).not.toContainHTML(left_title);
-    expect(region[1]).not.toContainHTML(left_description);
+    expect(region[1]).toContainHTML(rightTitle);
+    expect(region[1]).toContainHTML(rightDescription);
+    expect(region[1]).not.toContainHTML(leftTitle);
+    expect(region[1]).not.toContainHTML(leftDescription);

Also applies to: 189-199, 200-216, 218-222, 225-237


218-220: Prefer screen over wrapper for queries

Testing Library recommends using screen to avoid passing around render results and to align with user-centric queries. Minor consistency improvement.

Apply this diff:

-    const left_button = wrapper.getByTestId("left-button");
-    const right_button = wrapper.getByTestId("right-button");
+    const left_button = screen.getByTestId("left-button");
+    const right_button = screen.getByTestId("right-button");

178-238: Consider adding coverage for close APIs scoped by toasterId

Optional but valuable: add tests exercising closeToast/closeAll with a toasterId to ensure they only affect the targeted provider’s queue. This would lock in the per-toaster isolation for both add and removal paths.

If helpful, I can draft a companion test that renders two providers, creates one toast in each, calls closeAll({toasterId: "left"}), and asserts only the left toast is removed.

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 64f5fcb and e284eaa.

📒 Files selected for processing (2)
  • packages/components/toast/__tests__/toast.test.tsx (1 hunks)
  • packages/components/toast/stories/toast.stories.tsx (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/components/toast/stories/toast.stories.tsx
🧰 Additional context used
🧬 Code Graph Analysis (1)
packages/components/toast/__tests__/toast.test.tsx (2)
packages/components/toast/src/toast-provider.tsx (2)
  • ToastProvider (40-68)
  • addToast (70-76)
packages/components/toast/src/index.ts (2)
  • ToastProvider (13-13)
  • addToast (9-9)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: Prettier
  • GitHub Check: Continuous Release
  • GitHub Check: ESLint
  • GitHub Check: TypeScript
  • GitHub Check: Tests
  • GitHub Check: Build
🔇 Additional comments (1)
packages/components/toast/__tests__/toast.test.tsx (1)

178-216: Multi-provider isolation test is solid

Good end-to-end coverage proving that distinct ToastProvider instances render only their own toasts when addressed via toasterId. This guards against the regression that originally duplicated toasts across providers.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
apps/docs/content/components/toast/index.ts (1)

9-9: Nit: consider normalizing filename casing to kebab-case

Other entries here use lowercase kebab-case (e.g., custom-styles, custom-close-icon). For consistency, consider renaming the files to multiple-toast-provider.* and updating this import accordingly.

Apply this change if you proceed with the rename:

-import multipleToastProvider from "./multiple-ToastProvider";
+import multipleToastProvider from "./multiple-toast-provider";
apps/docs/content/components/toast/multiple-ToastProvider.raw.jsx (1)

10-13: Nit: wrapper fixed container may be redundant

If ToastProvider renders via a portal and already handles its own positioning/z-index, the outer <div className="fixed z-[100]"> is unnecessary. Consider removing it to avoid creating an extra stacking context.

Suggested edit:

-      <div className="fixed z-[100]">
-        <ToastProvider placement="top-right" toastOffset={60} toasterId={topToasterId} />
-        <ToastProvider placement="bottom-right" toasterId={bottomToasterId} />
-      </div>
+      <ToastProvider placement="top-right" toastOffset={60} toasterId={topToasterId} />
+      <ToastProvider placement="bottom-right" toasterId={bottomToasterId} />

If ToastProvider does not portal, keep the wrapper or move fixed/z-index handling into the provider(s) for consistency.

apps/docs/content/components/toast/multiple-ToastProvider.ts (1)

1-1: Nit: align import path casing if you rename the files

If you adopt kebab-case for the demo filenames, update this import accordingly:

-import App from "./multiple-ToastProvider.raw.jsx?raw";
+import App from "./multiple-toast-provider.raw.jsx?raw";
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 875657f and c6ab38f.

📒 Files selected for processing (5)
  • apps/docs/content/components/toast/index.ts (2 hunks)
  • apps/docs/content/components/toast/multiple-ToastProvider.raw.jsx (1 hunks)
  • apps/docs/content/components/toast/multiple-ToastProvider.ts (1 hunks)
  • apps/docs/content/docs/components/toast.mdx (1 hunks)
  • packages/components/toast/stories/toast.stories.tsx (2 hunks)
✅ Files skipped from review due to trivial changes (1)
  • apps/docs/content/docs/components/toast.mdx
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/components/toast/stories/toast.stories.tsx
🧰 Additional context used
🧬 Code Graph Analysis (1)
apps/docs/content/components/toast/multiple-ToastProvider.ts (1)
apps/docs/content/components/toast/multiple-ToastProvider.raw.jsx (1)
  • App (4-42)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: TypeScript
  • GitHub Check: Continuous Release
  • GitHub Check: Build
  • GitHub Check: Tests
🔇 Additional comments (5)
apps/docs/content/components/toast/index.ts (2)

9-9: Docs wiring for multi-toaster example looks good

The new import is correct and matches the filename in this folder.


20-21: Export surface updated correctly

Including multipleToastProvider in toastContent makes the demo available to the docs. Looks good.

apps/docs/content/components/toast/multiple-ToastProvider.raw.jsx (2)

1-2: Imports and API usage are aligned with multi-toaster design

Using ToastProvider twice with distinct toasterIds and routing addToast calls via the same IDs demonstrates the feature cleanly.


18-23: Targeted toasts via toasterId are correct

Both handlers correctly route toasts to their intended providers using the matching toasterId. Clear and minimal example.

Also applies to: 29-35

apps/docs/content/components/toast/multiple-ToastProvider.ts (1)

1-9: Re-export pattern for raw demo content is consistent

The mapping to expose “/App.jsx” for the CodeDemo/Sandpack setup matches existing docs patterns. Looks good.

@ChaserZ98 ChaserZ98 requested a review from wingkwong August 18, 2025 11:48
Copy link
Member

@wingkwong wingkwong left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please resolve the conflicts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants