Skip to content

fix(toast): prevent duplicate toasts when multiple providers are declared#5590

Closed
KumJungMin wants to merge 5 commits into
heroui-inc:canaryfrom
KumJungMin:fix/issue-5559_
Closed

fix(toast): prevent duplicate toasts when multiple providers are declared#5590
KumJungMin wants to merge 5 commits into
heroui-inc:canaryfrom
KumJungMin:fix/issue-5559_

Conversation

@KumJungMin
Copy link
Copy Markdown
Contributor

@KumJungMin KumJungMin commented Aug 9, 2025

Closes #5559

📝 Description

  • Removes the extra <ToastProvider> declared in the example,
  • which was causing duplicate toasts when combined with the global provider.
  • This change ensures only the global provider instance is used in the docs/examples.

before

2025-08-09.10.24.21.mov

after

after.mov

⛳️ Current behavior (updates)

  • Multiple <ToastProvider> declarations in the app cause the toast queue to render duplicates,
  • as each provider instance manages its own region rendering.

🚀 New behavior

  • Only the global provider instance is used.
  • The example no longer declares its own <ToastProvider>, preventing duplicate toast rendering.

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

  • No

📝 Additional Information

  • As a future improvement, we can make <ToastProvider> internally manage a global singleton toast queue so that even if multiple providers are declared, they share the same instance and avoid duplicates:
const G = globalThis as any;
const QUEUE_KEY = "__heroui_toast_queue__";

export const getToastQueue = () => {
  if (!G[QUEUE_KEY]) {
    G[QUEUE_KEY] = new ToastQueue<ToastProps>({ maxVisibleToasts: Infinity });
  }
  return G[QUEUE_KEY];
};
  • If the team agrees with this approach, I can apply it in a follow-up PR :)

Summary by CodeRabbit

  • Refactor
    • Toast notifications now appear only on blog pages (paths under /blog).
    • Other areas of the site no longer display or mount the toast container, reducing unexpected pop-ups outside the blog.
    • Existing toast behavior and interactions on blog pages remain unchanged.
    • No changes to public component APIs; users should experience a more focused UI with toasts confined to relevant blog content.

@KumJungMin KumJungMin requested a review from jrgarciadev as a code owner August 9, 2025 13:21
@vercel
Copy link
Copy Markdown

vercel Bot commented Aug 9, 2025

@KumJungMin 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
Copy Markdown
Contributor

coderabbitai Bot commented Aug 9, 2025

Walkthrough

Introduces a state flag to detect blog pages via window.location.pathname on mount and conditionally renders ToastProvider only when on a /blog path. Existing toast state and handlers remain unchanged.

Changes

Cohort / File(s) Summary
Toast docs example
apps/docs/content/components/toast/close.raw.jsx
Add isBlogPage state initialized in useEffect using window.location.pathname.startsWith("/blog"); render ToastProvider only when isBlogPage is true; retain existing toastKey logic and imports.

Sequence Diagram(s)

sequenceDiagram
  participant U as User
  participant App as App (close.raw.jsx)
  participant WL as window.location
  participant TP as ToastProvider

  U->>App: Load component
  App->>WL: Check pathname on mount
  WL-->>App: "/...path..."
  alt Path starts with "/blog"
    App->>TP: Mount ToastProvider
  else
    App--xTP: Do not mount
  end

  U->>App: Click toast trigger
  App->>TP: Show toast (if mounted)
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~8 minutes

Assessment against linked issues

Objective Addressed Explanation
Fix duplicate toasts on docs page; ensure a single toast per click (#5559) No direct assertion or coverage; conditional render tied to /blog path may not clearly address docs page duplication.

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
Introduce blog-path-based conditional mounting for ToastProvider (apps/docs/content/components/toast/close.raw.jsx) The linked issue concerns duplicate toasts on the docs toast page, not blog pages. Conditioning on /blog appears unrelated to the stated objective.

Possibly related PRs

Suggested labels

👀 Status: To Review

Suggested reviewers

  • jrgarciadev
  • wingkwong
  • macci001
✨ 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.

Copy link
Copy Markdown
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: 1

🧹 Nitpick comments (4)
.changeset/empty-needles-hope.md (2)

5-5: Polish the release note for clarity and traceability

Consider imperative mood and referencing the issue.

-prevent duplicate toasts when multiple providers are declared
+Prevent duplicate toasts when multiple ToastProvider instances are declared (#5559)

1-5: Confirm that a package release is intended for a docs-only fix

This PR shows a docs example change. If no code changed in @heroui/toast, consider omitting the changeset to avoid publishing a no-op release. If library changes exist elsewhere in the PR, ignore this.

Happy to adjust the changeset scope/message once confirmed.

apps/docs/content/components/toast/close.raw.jsx (2)

9-9: Remove leftover empty wrapper div

The fixed, z-indexed div is now empty and adds an unnecessary DOM node.

-      <div className="fixed z-[100]" />
+      {/* Intentionally no local ToastProvider here to avoid duplicates; relies on the global provider */}

If no comment is needed, simply delete the line.


28-28: Use strict equality

Prefer === to avoid implicit coercion.

-            if (toastKey.length == 0) return;
+            if (toastKey.length === 0) return;
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 8e51a85 and 803b906.

📒 Files selected for processing (2)
  • .changeset/empty-needles-hope.md (1 hunks)
  • apps/docs/content/components/toast/close.raw.jsx (1 hunks)
⏰ 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: Continuous Release
  • GitHub Check: TypeScript
  • GitHub Check: ESLint
  • GitHub Check: Build
🔇 Additional comments (2)
.changeset/empty-needles-hope.md (1)

1-3: Changeset frontmatter looks valid

Package + patch bump are correctly declared.

apps/docs/content/components/toast/close.raw.jsx (1)

1-1: Good: removed local ToastProvider from the example

Aligns the example with a single global provider to prevent duplicates.

@@ -1,14 +1,12 @@
import {addToast, Button, closeToast, closeAll, ToastProvider} from "@heroui/react";
import {addToast, Button, closeToast, closeAll} from "@heroui/react";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Verify a single global ToastProvider exists in the app shell

Since the example now relies on a global provider, ensure the docs app has exactly one ToastProvider at the root.


🏁 Script executed:

#!/bin/bash
# Find all occurrences of ToastProvider across the repo
rg -n "<ToastProvider" -S -A 2

# Count unique files containing ToastProvider
rg -n "<ToastProvider" -S | cut -d: -f1 | sort -u | wc -l

Length of output: 5888


Ensure a single global ToastProvider at the docs app root

I spotted two providers active in the docs project:

  • In apps/docs/app/docs/[[...slug]]/page.tsx (conditionally rendered per page)
  • In apps/docs/content/docs/components/toast.mdx (wraps only the Toast MDX content)

Since examples now rely on a global provider, consolidate into one root‐level provider:

• In apps/docs/app/docs/[[...slug]]/page.tsx
– Remove the condition around <ToastProvider /> (i.e. drop doc.title !== "Toast").
– Wrap all page content unconditionally:
tsx <ToastProvider> {/* your MDX content rendering (children) */} </ToastProvider>

• In apps/docs/content/docs/components/toast.mdx
– Delete the <ToastProvider /> wrapper (around lines 53–55).

After that, the docs app will have exactly one ToastProvider at its root.

🤖 Prompt for AI Agents
In apps/docs/content/components/toast/close.raw.jsx at line 1 and related files,
there are multiple ToastProvider instances causing redundancy. Remove the
conditional ToastProvider wrapper in apps/docs/app/docs/[[...slug]]/page.tsx by
eliminating the condition around <ToastProvider /> and wrap all page content
unconditionally with <ToastProvider>. Then, delete the <ToastProvider /> wrapper
from apps/docs/content/docs/components/toast.mdx around lines 53 to 55 to ensure
only one global ToastProvider exists at the root of the docs app.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Aug 9, 2025

Open in StackBlitz

@heroui/accordion

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

@heroui/alert

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

@heroui/autocomplete

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

@heroui/avatar

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

@heroui/badge

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

@heroui/breadcrumbs

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

@heroui/button

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

@heroui/calendar

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

@heroui/card

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

@heroui/checkbox

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

@heroui/chip

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

@heroui/code

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

@heroui/date-input

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

@heroui/date-picker

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

@heroui/divider

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

@heroui/drawer

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

@heroui/dropdown

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

@heroui/form

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

@heroui/image

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

@heroui/input

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

@heroui/input-otp

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

@heroui/kbd

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

@heroui/link

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

@heroui/listbox

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

@heroui/menu

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

@heroui/modal

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

@heroui/navbar

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

@heroui/number-input

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

@heroui/pagination

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

@heroui/popover

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

@heroui/progress

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

@heroui/radio

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

@heroui/ripple

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

@heroui/scroll-shadow

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

@heroui/select

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

@heroui/skeleton

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

@heroui/slider

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

@heroui/snippet

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

@heroui/spacer

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

@heroui/spinner

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

@heroui/switch

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

@heroui/table

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

@heroui/tabs

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

@heroui/toast

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

@heroui/tooltip

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

@heroui/user

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

@heroui/react

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

@heroui/system

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

@heroui/system-rsc

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

@heroui/theme

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

@heroui/use-aria-accordion

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

@heroui/use-aria-accordion-item

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

@heroui/use-aria-button

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

@heroui/use-aria-link

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

@heroui/use-aria-modal-overlay

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

@heroui/use-aria-multiselect

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

@heroui/use-aria-overlay

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

@heroui/use-callback-ref

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

@heroui/use-clipboard

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

@heroui/use-data-scroll-overflow

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

@heroui/use-disclosure

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

@heroui/use-draggable

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

@heroui/use-form-reset

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

@heroui/use-image

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

@heroui/use-infinite-scroll

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

@heroui/use-intersection-observer

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

@heroui/use-is-mobile

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

@heroui/use-is-mounted

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

@heroui/use-measure

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

@heroui/use-pagination

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

@heroui/use-real-shape

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

@heroui/use-ref-state

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

@heroui/use-resize

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

@heroui/use-safe-layout-effect

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

@heroui/use-scroll-position

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

@heroui/use-ssr

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

@heroui/use-theme

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

@heroui/use-update-effect

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

@heroui/use-viewport-size

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

@heroui/aria-utils

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

@heroui/dom-animation

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

@heroui/framer-utils

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

@heroui/react-rsc-utils

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

@heroui/react-utils

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

@heroui/shared-icons

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

@heroui/shared-utils

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

@heroui/stories-utils

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

@heroui/test-utils

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

commit: b6bf10d

@vercel
Copy link
Copy Markdown

vercel Bot commented Aug 9, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
heroui ✅ Ready (Inspect) Visit Preview 💬 Add feedback Aug 9, 2025 4:21pm
heroui-sb ✅ Ready (Inspect) Visit Preview 💬 Add feedback Aug 9, 2025 4:21pm

Copy link
Copy Markdown
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. if you just update doc, then no need to add changeset.
  2. the changes would make the example https://heroui-rheco9d3p-heroui.vercel.app/blog/v2.8.0#toast not working. I think that is the reason why ToastProvider was included in close.raw.jsx.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Aug 13, 2025

⚠️ No Changeset found

Latest commit: b6bf10d

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

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

@KumJungMin
Copy link
Copy Markdown
Contributor Author

updated commit: b6bf10d

Previously, ToastProvider was being rendered in both the blog page and
the higher-level layout, causing it to be registered twice.

Why not use a global “single registration” guard?

  • A global guard (e.g., via globalThis or singleton variables) would
    prevent multiple registrations but also reduce flexibility.
  • Some pages may require different Provider configurations or dynamic
    removal/re-addition, which a forced single registration would block.
  • Keeping control at the application level is more maintainable.

Why use the current pathname-based approach?

  • Restricting Provider rendering to when pathname starts with /blog
    prevents duplicate registration and avoids loading it on unrelated
    pages.
  • This approach is simple, keeps logic in the app, and solves the issue
    without adding complexity to the library.

@KumJungMin KumJungMin requested a review from wingkwong August 13, 2025 13:48
Copy link
Copy Markdown
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: 2

🔭 Outside diff range comments (1)
apps/docs/content/components/toast/close.raw.jsx (1)

1-1: Update imports after removing local provider

Once the local provider is removed, drop it from the import to avoid an unused import.

-import {addToast, Button, closeToast, closeAll, ToastProvider} from "@heroui/react";
+import {addToast, Button, closeToast, closeAll} from "@heroui/react";
🧹 Nitpick comments (1)
apps/docs/content/components/toast/close.raw.jsx (1)

33-35: Use strict equality

Use strict equality for clarity and consistency.

-            if (toastKey.length == 0) return;
+            if (toastKey.length === 0) return;
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 803b906 and b6bf10d.

📒 Files selected for processing (1)
  • apps/docs/content/components/toast/close.raw.jsx (1 hunks)
⏰ 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: TypeScript
  • GitHub Check: Prettier
  • GitHub Check: ESLint
  • GitHub Check: Continuous Release
  • GitHub Check: Tests
  • GitHub Check: Build

Comment on lines +6 to +10
const [isBlogPage, setIsBlogPage] = React.useState(false);

React.useEffect(() => {
setIsBlogPage(window.location.pathname.startsWith("/blog"));
}, []);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Route-based gating of ToastProvider undermines the fix; remove it and rely on the global provider

This keeps a second provider active on blog pages, so duplicate toasts will still occur there (after hydration). It also adds a client-only route check that creates an SSR/hydration timing window where the local provider flips on post-mount. The PR objective is to have a single global provider; this local provider should be removed entirely, not conditionally rendered.

Apply this diff to delete the route-coupled state/effect:

-  const [isBlogPage, setIsBlogPage] = React.useState(false);
-
-  React.useEffect(() => {
-    setIsBlogPage(window.location.pathname.startsWith("/blog"));
-  }, []);

Run this script to verify there are no example-local providers left and that only one provider exists in the docs app shell:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "All occurrences of <ToastProvider> across the repo:"
rg -n -S '<ToastProvider'

echo
echo "Occurrences under docs content (examples/snippets) — expected: 0"
rg -n -S '<ToastProvider' apps/docs/content || true

echo
echo "Occurrences under docs app shell — expected: exactly 1 (layout/providers)"
rg -n -S '<ToastProvider' apps/docs/app -A 2 || true

Length of output: 3811


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Showing 15 lines before and after the global provider condition in page.tsx:"
rg -n 'ToastProvider' -A15 -B15 apps/docs/app/docs/[[...slug]]/page.tsx

Length of output: 1711


Remove client-only route gating and render the provider unconditionally in the close example

The isBlogPage state + effect creates an SSR/hydration mismatch and prevents the example from always having a provider. Since MDX renders raw examples before the global provider anyway, the local <ToastProvider /> must remain—but without the route check.

• File: apps/docs/content/components/toast/close.raw.jsx
• Remove the useState/useEffect for isBlogPage
• Change the provider wrapper to always render

Apply this diff:

-import React from "react";
+import React from "react";
 import { Toast, ToastProvider } from "@heroui/react";

-export default function ToastCloseExample() {
-  const [isBlogPage, setIsBlogPage] = React.useState(false);
-
-  React.useEffect(() => {
-    setIsBlogPage(window.location.pathname.startsWith("/blog"));
-  }, []);
+export default function ToastCloseExample() {
   return (
     <>
       <button onClick={() => Toast.close()}>Show Toast</button>
       <Toast description="Closable toast" closeable />
-      <div className="fixed z-[100]">{isBlogPage && <ToastProvider />}</div>
+      <div className="fixed z-[100]"><ToastProvider /></div>
     </>
   );
 }
🤖 Prompt for AI Agents
In apps/docs/content/components/toast/close.raw.jsx around lines 6 to 10, remove
the client-only route gating: delete the React.useState and React.useEffect that
compute isBlogPage (the window.location.pathname check) and update the JSX to
always render the <ToastProvider> wrapper (no conditional based on isBlogPage)
so the provider is present unconditionally in the example to avoid SSR/hydration
mismatch.

<div className="fixed z-[100]">
<ToastProvider />
</div>
<div className="fixed z-[100]">{isBlogPage && <ToastProvider />}</div>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Delete the local ToastProvider; rely on the single global provider

The fixed container plus a local provider is the root cause of duplicate regions. Remove this line and let the global provider handle the toast region.

-      <div className="fixed z-[100]">{isBlogPage && <ToastProvider />}</div>
+      {/* ToastProvider is provided globally at the app root */}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div className="fixed z-[100]">{isBlogPage && <ToastProvider />}</div>
{/* ToastProvider is provided globally at the app root */}
🤖 Prompt for AI Agents
In apps/docs/content/components/toast/close.raw.jsx around line 14, the
component renders a fixed container with a local <ToastProvider> which creates a
duplicate toast region; remove this line (the fixed div wrapping and the local
ToastProvider) so the component no longer mounts a local provider and instead
relies on the single global ToastProvider already mounted at the app level.

@ChaserZ98
Copy link
Copy Markdown

ChaserZ98 commented Aug 15, 2025

  1. the changes would make the example https://heroui-rheco9d3p-heroui.vercel.app/blog/v2.8.0#toast not working. I think that is the reason why ToastProvider was included in close.raw.jsx.

@wingkwong I am the one who wrote this.
That ToastProvider in the close.raw.jsx is essential for storybook test cases to work so simply removing that won't work.The duplicate toast is probably caused by another ToastProvider used by the doc website itself IMO.
I double-checked the original PR (#5125). The ToastProvider wasn't present there so possibly someone else added it to solve the blog issue you mentioned but unluckily caused this issue on the doc. I couldn't find a clue about this change from the commit history of this file as it is all stacked up in #5473. Maybe you can help digging it out here?
Okay, I found it here (7f5b8e2). Kind of awkward bro :)

@KumJungMin Your solution may fix the duplicate toast issue on the doc. But it would also add some unrelated code to the toast example since the preview and the code section of the doc share the same code (if I remember correctly). Users may be confused to see this.

@ChaserZ98
Copy link
Copy Markdown

ChaserZ98 commented Aug 16, 2025

Okay, I digged a bit in the doc code.
THERE IS NO GLOBAL ToastProvider INSTANCE.
If you removed the current ToastProvider in the close.raw.jsx file, the one that makes all the other toast working is the one in the placement.raw.jsx file. If you remove that one as well, boom, none of the toast will show up.

@wingkwong The blog one doesn't work because it doesn't include the code demo from placement.raw.jsx which contains the ToastProvider. That's why you want to include another ToastProvider in the close.raw.jsx, which caused the duplicate toast problem in the doc.

The problem behind this is that all the ToastProvider instances share the same ToastQueue. The addToast method puts the toast into that queue and all the ToastProvider instances will render the UI effects based on that queue. So basically if you have n ToastProvider instances on the current page, calling addToast once will add one toast into the ToastQueue but n ToastProvider instances will render it n times.

Maybe we could adopt the multi toaster design from react-hot-toast. We can assign each ToastProvider a unique id toasterId and let addToast accept a toasterId field so that each ToastProvider instance could know which toast in the ToastQueue belongs to it.

@wingkwong
Copy link
Copy Markdown
Member

Closing - agree with the multi toaster approach.

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.

[BUG] - Extra Toast

3 participants