Toast does not always appear after immediately hiding it
Do you want to request a feature or report a bug? Bug
What is the current behavior?
Been facing some weird toast show/hide behavior so tried to reproduce it in clean project (codesandbox link attached). The problem is multiple fold (but possibly related):
- Regarding provided codesandbox example - toast does not always show after showing it in
componentDidMountand hiding it incomponentWillUnmount
a) with 7.x in my production app (cannot share code or screens) there seems to be some problem with multiple containers where upon dismissing toast it fails to destroy the underlying ToastContainer element and it gets permanently visible in app
b) with 6.x with same scenario (showing it in componentDidMount and hiding it in componentWillUnmount) it does seem that react-toast executes asynchronously hide-show-hide even though I am logging from app point of view only hide-show - which results in toast being shown and immediately hidden
If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem. Your bug will get fixed much faster if we can run your code and it doesn't have dependencies other than React. Paste the link to your CodeSandbox (https://codesandbox.io/s/new) example below:
https://codesandbox.io/s/wandering-morning-qwrbh?file=/src/App.js
What is the expected behavior?
Which versions of React, and which browser / OS are affected by this issue? Did this work in previous versions of React?
faced the same issue
Same here. Was using "react-toastify": "^5.5.0", and clicking the same button to launch a toast would re-launch the toast. Now with v7 I have to wait 500ms or so between clicks. This is breaking my tests.
I experience a ~500 ms delay between toast.isActive(id) becoming false, and options.onClose callback triggering (onClose triggers second), this seems to be highly related.
I made a workaround that tracks the open status separately, and re-dispatches toast after onClose gets called. It's not ideal (there is a visual delay, because we need to wait for the inital toast to actually finish closing internally), but it's good enough for me, and required minimal effort.
Hope this is useful to someone:
import { ToastOptions } from 'react-toastify'
type ToastId = ToastOptions['toastId']
const openIds = new Set<ToastId>()
export const isToastOpen = (id?: ToastId): id is ToastId => !!id && openIds.has(id)
const registerOpenToast = (id?: ToastId) => id && openIds.add(id)
const unregisterOpenToast = (id?: ToastId) => id && openIds.delete(id)
export const bindStatusListenersToOptions = (options?: ToastOptions): ToastOptions => {
return {
...options,
onClose: (params: any) => {
unregisterOpenToast(options?.toastId)
options?.onClose?.(params)
},
onOpen: (params: any) => {
registerOpenToast(options?.toastId)
options?.onOpen?.(params)
}
}
}
export const waitUntilToastIsClosed = (id?: ToastId) => {
const TIMEOUT_THRESHOLD = 3000
const QUERY_INTERVAL = 50
return new Promise<void>((resolve, reject) => {
const initialTimestamp = Date.now()
const intervalId = setInterval(() => {
if (Date.now() - initialTimestamp > TIMEOUT_THRESHOLD) {
clearInterval(intervalId)
reject()
}
if (!isToastOpen(id)) {
clearInterval(intervalId)
resolve()
}
}, QUERY_INTERVAL)
})
}
And then, when dispatching the toast:
import {
bindStatusListenersToOptions,
isToastOpen,
waitUntilToastIsClosed
} from '../utils/openStatusTracker'
const dispatchToast = (content: ReactNode, options: ToastOptions) => {
const optionsWithListeners = bindStatusListenersToOptions(options)
const id = options?.toastId
// This is the problematic state when dispatching is bugged
if(id && isToastOpen(id) && !toast.isActive(id)) {
waitUntilToastIsClosed
.then(() => dispatchToast(content, options))
.catch()
return
}
toast(content, options)
}