react-toastify icon indicating copy to clipboard operation
react-toastify copied to clipboard

Toast does not always appear after immediately hiding it

Open UncleInf opened this issue 4 years ago • 3 comments

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):

  1. Regarding provided codesandbox example - toast does not always show after showing it in componentDidMount and hiding it in componentWillUnmount

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?

UncleInf avatar Apr 26 '21 19:04 UncleInf

faced the same issue

anton-danilov-neurio avatar May 20 '21 15:05 anton-danilov-neurio

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.

pstephenwille avatar May 28 '21 20:05 pstephenwille

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)
}

michal-kurz avatar Sep 08 '22 15:09 michal-kurz