fix(toast): prevent duplicate toasts when multiple providers are declared#5590
fix(toast): prevent duplicate toasts when multiple providers are declared#5590KumJungMin wants to merge 5 commits into
Conversation
|
@KumJungMin is attempting to deploy a commit to the HeroUI Inc Team on Vercel. A member of the Team first needs to authorize it. |
WalkthroughIntroduces 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
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)
Estimated code review effort🎯 2 (Simple) | ⏱️ ~8 minutes Assessment against linked issues
Assessment against linked issues: Out-of-scope changes
Possibly related PRs
Suggested labels
Suggested reviewers
✨ Finishing Touches
🧪 Generate unit tests
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
Status, Documentation and Community
|
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (4)
.changeset/empty-needles-hope.md (2)
5-5: Polish the release note for clarity and traceabilityConsider 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 fixThis 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 divThe 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 equalityPrefer
===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
📒 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 validPackage + patch bump are correctly declared.
apps/docs/content/components/toast/close.raw.jsx (1)
1-1: Good: removed local ToastProvider from the exampleAligns 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"; | |||
There was a problem hiding this comment.
💡 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 -lLength 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.
@heroui/accordion
@heroui/alert
@heroui/autocomplete
@heroui/avatar
@heroui/badge
@heroui/breadcrumbs
@heroui/button
@heroui/calendar
@heroui/card
@heroui/checkbox
@heroui/chip
@heroui/code
@heroui/date-input
@heroui/date-picker
@heroui/divider
@heroui/drawer
@heroui/dropdown
@heroui/form
@heroui/image
@heroui/input
@heroui/input-otp
@heroui/kbd
@heroui/link
@heroui/listbox
@heroui/menu
@heroui/modal
@heroui/navbar
@heroui/number-input
@heroui/pagination
@heroui/popover
@heroui/progress
@heroui/radio
@heroui/ripple
@heroui/scroll-shadow
@heroui/select
@heroui/skeleton
@heroui/slider
@heroui/snippet
@heroui/spacer
@heroui/spinner
@heroui/switch
@heroui/table
@heroui/tabs
@heroui/toast
@heroui/tooltip
@heroui/user
@heroui/react
@heroui/system
@heroui/system-rsc
@heroui/theme
@heroui/use-aria-accordion
@heroui/use-aria-accordion-item
@heroui/use-aria-button
@heroui/use-aria-link
@heroui/use-aria-modal-overlay
@heroui/use-aria-multiselect
@heroui/use-aria-overlay
@heroui/use-callback-ref
@heroui/use-clipboard
@heroui/use-data-scroll-overflow
@heroui/use-disclosure
@heroui/use-draggable
@heroui/use-form-reset
@heroui/use-image
@heroui/use-infinite-scroll
@heroui/use-intersection-observer
@heroui/use-is-mobile
@heroui/use-is-mounted
@heroui/use-measure
@heroui/use-pagination
@heroui/use-real-shape
@heroui/use-ref-state
@heroui/use-resize
@heroui/use-safe-layout-effect
@heroui/use-scroll-position
@heroui/use-ssr
@heroui/use-theme
@heroui/use-update-effect
@heroui/use-viewport-size
@heroui/aria-utils
@heroui/dom-animation
@heroui/framer-utils
@heroui/react-rsc-utils
@heroui/react-utils
@heroui/shared-icons
@heroui/shared-utils
@heroui/stories-utils
@heroui/test-utils
commit: |
|
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
wingkwong
left a comment
There was a problem hiding this comment.
- if you just update doc, then no need to add changeset.
- 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.
|
Previously, ToastProvider was being rendered in both the blog page and Why not use a global “single registration” guard?
Why use the current pathname-based approach?
|
There was a problem hiding this comment.
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 providerOnce 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 equalityUse 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
📒 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
| const [isBlogPage, setIsBlogPage] = React.useState(false); | ||
|
|
||
| React.useEffect(() => { | ||
| setIsBlogPage(window.location.pathname.startsWith("/blog")); | ||
| }, []); |
There was a problem hiding this comment.
💡 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 || trueLength 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.tsxLength 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> |
There was a problem hiding this comment.
🛠️ 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.
| <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.
@wingkwong I am the one who wrote this. @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. |
|
Okay, I digged a bit in the doc code. @wingkwong The blog one doesn't work because it doesn't include the code demo from The problem behind this is that all the Maybe we could adopt the multi toaster design from |
|
Closing - agree with the multi toaster approach. |
Closes #5559
📝 Description
<ToastProvider>declared in the example,before
2025-08-09.10.24.21.mov
after
after.mov
⛳️ Current behavior (updates)
<ToastProvider>declarations in the app cause the toast queue to render duplicates,🚀 New behavior
<ToastProvider>, preventing duplicate toast rendering.💣 Is this a breaking change (Yes/No):
📝 Additional Information
<ToastProvider>internally manage a global singleton toast queue so that even if multiple providers are declared, they share the same instance and avoid duplicates:Summary by CodeRabbit