Skip to content

Conversation

@portikM
Copy link
Member

@portikM portikM commented Dec 5, 2025

Summary

Addresses: https://konghq.atlassian.net/browse/KHCP-18535

Second attempt at preventing ToastManager from creating duplicate containers. Fixing issues introduced in #2980 and reverted in #3015.

@portikM portikM self-assigned this Dec 5, 2025
@netlify
Copy link

netlify bot commented Dec 5, 2025

Deploy Preview for kongponents-sandbox ready!

Name Link
🔨 Latest commit 367b6ad
🔍 Latest deploy log https://app.netlify.com/projects/kongponents-sandbox/deploys/69664f0fac71300008bb03fb
😎 Deploy Preview https://deploy-preview-3017--kongponents-sandbox.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify
Copy link

netlify bot commented Dec 5, 2025

Deploy Preview for kongponents ready!

Name Link
🔨 Latest commit 367b6ad
🔍 Latest deploy log https://app.netlify.com/projects/kongponents/deploys/69664f0f2c12eb0007323ece
😎 Deploy Preview https://deploy-preview-3017--kongponents.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@johncowen
Copy link
Contributor

johncowen commented Jan 8, 2026

hey @portikM 👋 , sorry I know you are up the wall with other stuff at the moment, if you get a sec could you add a bit of detail in the description here what the "issues introduced" were exactly, thanks!

Second attempt at preventing ToastManager from creating duplicate containers. Fixing issues introduced in #2980 and reverted in #3015.

It looks like this PR is just a direct copy of the reverted one, and from that I guess you are planning on working on top of the reverted one is that understanding correct?

@portikM
Copy link
Member Author

portikM commented Jan 8, 2026

hey @portikM 👋 , sorry I know you are up the wall with other stuff at the moment, if you get a sec could you add a bit of detail in the description here what the "issues introduced" were exactly, thanks!

Second attempt at preventing ToastManager from creating duplicate containers. Fixing issues introduced in #2980 and reverted in #3015.

It looks like this PR is just a direct copy of the reverted one, and from that I guess you are planning on working on top of the reverted one is that understanding correct?

Hi @johncowen! Yes I had to deprioritize this for a bit but I am picking this back up today. The issue with this implementation came up in this thread. I didn't have time to identify exactly why, but essentially the toast messages weren't showing sometimes, our regression tests caught it. I still think this approach is good, just need to debug and fix the issue.

@johncowen
Copy link
Contributor

Thanks Max!

Not sure if this helps, but we had a quick look at this yesterday and noticed that there's an opportunity for unintended behaviour here if you have 2 individual ToastManagers. One example is:

Given 2 ToastManagers:

  1. open is called on ToastManager 1
  2. time passes
  3. open is called on ToastManager 2
  4. a tick passes
  5. destroy is called from ToastManager 1
  6. The toast from ToastManager 2 is removed from the DOM and was only "live" for a tick - i.e. it was never shown

I think the issue is worse on the current code on main (previous to this PR) because the DOM target it created on construction rather than open - but still I saw that some tests are expecting toasts to be in the DOM and they are failing due the tests not being able to find the toast. While this could explain the problem there, it doesn't explain why your upstream application tests were fine beforehand, unless I'm missing something 🤔

We found only two instances (in upstream applications aside from kuma) where the toast managers being created actually have destroy called on them. FYI: in kuma, we are not currently calling destroy at all but "ideally" things would get cleaned up properly.

I guess if we don't want to go down the lifecycle tracking route, another option would be to just provide the toast manager from a single "shell" application instead of having individual applications instantiate a toast manager themselves. Application would then access the manager provided by the "shell" and the shell would be responsible for cleaning up. Of course if we aren't super precious about cleaning up (after all we want the toast manager to be available throughout the lifetime of the app) we could skip that (ideally we wouldn't skip cleanup, but maybe pragmatism wins here if we don't want to manage lifecycle)

This option would solve it for our specific needs, but obvs not for others (not sure how many other folks are encountering this issue with duplicate toast managers) - I guess a "fix" there would be to document that ToastManager should be implemented as a singleton in your application and then destroy should only be called once you are sure all toast managers are "finished" (if at all) to prevent these sorts of shared state issues.

Anyway, thought I'd share our findings/thoughts from yesterday, none of the above are blocking asks or anything

@portikM
Copy link
Member Author

portikM commented Jan 9, 2026

Thanks Max!

Not sure if this helps, but we had a quick look at this yesterday and noticed that there's an opportunity for unintended behaviour here if you have 2 individual ToastManagers. One example is:

Given 2 ToastManagers:

  1. open is called on ToastManager 1
  2. time passes
  3. open is called on ToastManager 2
  4. a tick passes
  5. destroy is called from ToastManager 1
  6. The toast from ToastManager 2 is removed from the DOM and was only "live" for a tick - i.e. it was never shown

I think the issue is worse on the current code on main (previous to this PR) because the DOM target it created on construction rather than open - but still I saw that some tests are expecting toasts to be in the DOM and they are failing due the tests not being able to find the toast. While this could explain the problem there, it doesn't explain why your upstream application tests were fine beforehand, unless I'm missing something 🤔

We found only two instances (in upstream applications aside from kuma) where the toast managers being created actually have destroy called on them. FYI: in kuma, we are not currently calling destroy at all but "ideally" things would get cleaned up properly.

I guess if we don't want to go down the lifecycle tracking route, another option would be to just provide the toast manager from a single "shell" application instead of having individual applications instantiate a toast manager themselves. Application would then access the manager provided by the "shell" and the shell would be responsible for cleaning up. Of course if we aren't super precious about cleaning up (after all we want the toast manager to be available throughout the lifetime of the app) we could skip that (ideally we wouldn't skip cleanup, but maybe pragmatism wins here if we don't want to manage lifecycle)

This option would solve it for our specific needs, but obvs not for others (not sure how many other folks are encountering this issue with duplicate toast managers) - I guess a "fix" there would be to document that ToastManager should be implemented as a singleton in your application and then destroy should only be called once you are sure all toast managers are "finished" (if at all) to prevent these sorts of shared state issues.

Anyway, thought I'd share our findings/thoughts from yesterday, none of the above are blocking asks or anything

Thanks for sharing your findings jc! Yeah I guess once we get this working there is a slight chance of mishap when 1 instance of ToastManager is being destroyed at the exact moment when another dispatches a toast. On the other hand, once this finalized and ToastManager only mounts one kongponents-toaster-container, do we really need to remove the element from the DOM when destroying an instance? 🤔 There's nothing wrong with leaving it there since we know the next created instance of ToastManager won't mount a new element.

@johncowen
Copy link
Contributor

do we really need to remove the element from the DOM when destroying an instance? 🤔 There's nothing wrong with leaving it there since we know the next created instance of ToastManager won't mount a new element.

Yeah this is what we were wondering also 👍 . I guess for us this is fine, but maybe we need to explain this in the docs that if you want to destroy you should only do it when you are sure you application has totally finished (I guess some folks/usecase may want to ensure this happens?). This is a total "maybe we should add this docs" from me, will leave that decision up to you

@johncowen
Copy link
Contributor

Ah and we'd also have to communicate and change this in the two places we found it, if you need help finding ping me offline 👍

We found only two instances (in upstream applications aside from kuma) where the toast managers being created actually have destroy called on them

@portikM
Copy link
Member Author

portikM commented Jan 9, 2026

do we really need to remove the element from the DOM when destroying an instance? 🤔 There's nothing wrong with leaving it there since we know the next created instance of ToastManager won't mount a new element.

Yeah this is what we were wondering also 👍 . I guess for us this is fine, but maybe we need to explain this in the docs that if you want to destroy you should only do it when you are sure you application has totally finished (I guess some folks/usecase may want to ensure this happens?). This is a total "maybe we should add this docs" from me, will leave that decision up to you

Yeah totally. I was thinking we could add an optional attribute in destroy method, something like removeToastersContainer: boolean = false and document it's usage. What do you think?

Update: on the second thought, all destroy method does is removes the container - when we skip removing it there's no reason to call it at all. Maybe we could document that calling the method is optional and they should only call it when application has totally finished like you suggested.

@johncowen
Copy link
Contributor

Update: on the second thought, all destroy method does is removes the container - when we skip removing it there's no reason to call it at all. Maybe we could document that calling the method is optional and they should only call it when application has totally finished like you suggested.

Yeah agree 👍

We should chase up internally those apps that are calling destroy also.

(P.S. I'm still on the side of "ideally" we'd clean up, but considering everything I think this approach is the best way forwards)

* Destroys the ToastManager instance and removes the toasters container element from the DOM
* @param removeToastersContainer - Whether to remove the toasters container element from the DOM (defaults to false)
*/
public destroy(removeToastersContainer: boolean = false) {
Copy link
Member Author

@portikM portikM Jan 9, 2026

Choose a reason for hiding this comment

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

In Konnect where we can have multiple ToastManager on the same view, removing the toasters container element from the DOM can [very unlikely but can] lead to a race condition when one instance removes the container at the same moment as another instance is attempting to dispatch a notification.

Because a toaster instance will not create a new container if one already exists, I think it's no big deal if we just have the toasters container element in the DOM?

Original conversation

Copy link
Contributor

Choose a reason for hiding this comment

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

Would it make sense to cover this in the tests by adding an assertion for making sure that the DOM element is not being removed after calling destroy with the default param (or setting it to false)? 🙂

Copy link
Contributor

Choose a reason for hiding this comment

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

I think there's also a theoretical bug here also.

If I call destroy and then destroy(true) I don't think the DOM container will get cleaned up:

  • destroy() > this.toastersContainer = null
  • destroy(true) this.toastersContainer && removeToastersContainer > null && true === false

Copy link
Member Author

Choose a reason for hiding this comment

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

Both good points, pushed a fix for both.

Copy link
Contributor

Choose a reason for hiding this comment

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

Probably I wasn't clear enough, but there is still a missing test/assertion for the case of calling destroy() and that the container still exists. There is only an assertion that the container is removed after calling destroy(true). 🙂

Copy link
Member Author

Choose a reason for hiding this comment

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

Oh yup my miss. Added a test scenario for that now.

@portikM
Copy link
Member Author

portikM commented Jan 9, 2026

Update: on the second thought, all destroy method does is removes the container - when we skip removing it there's no reason to call it at all. Maybe we could document that calling the method is optional and they should only call it when application has totally finished like you suggested.

Yeah agree 👍

We should chase up internally those apps that are calling destroy also.

(P.S. I'm still on the side of "ideally" we'd clean up, but considering everything I think this approach is the best way forwards)

@johncowen fixed my implementation and added a note for what we talked about in the docs. This PR now ready for review.

Screenshot 2026-01-09 at 1 26 00 PM

@portikM portikM marked this pull request as ready for review January 9, 2026 18:29
@portikM portikM requested a review from adamdehaven as a code owner January 9, 2026 18:29
@portikM portikM requested review from a team, Justineo and jillztom as code owners January 9, 2026 18:29
Copy link
Member

@adamdehaven adamdehaven left a comment

Choose a reason for hiding this comment

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

question: is there any impact to the Nuxt module and the composable it exposes? (@arashsheyda @portikM )

/>
```

<KRadio card v-model="errorPropRadio" label="Input error" error description="Some description text" :selected-value="false" />
Copy link
Member

Choose a reason for hiding this comment

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

FYI: This is unrelated but a valid docs example we requested 👍🏼

@portikM
Copy link
Member Author

portikM commented Jan 9, 2026

question: is there any impact to the Nuxt module and the composable it exposes? (@arashsheyda @portikM )

I would imagine there's none but I can double check.

@arashsheyda
Copy link
Member

I don't think it it owuld effect at all, but also if it makes it easier we could setup a mini nuxt sandbox here for easier testing

}

private setupToastersContainer(): void {
const toastersContainerEl = document?.getElementById(toasterContainerId)
Copy link
Member

Choose a reason for hiding this comment

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

Can you pull the preview PR into portal? Just want to make sure this doesn't init on the server

Copy link
Member Author

Choose a reason for hiding this comment

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

Copy link
Contributor

@johncowen johncowen left a comment

Choose a reason for hiding this comment

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

Seeing as I've been involved in the convo a little here this LGTM! (considering we are fine not cleaning up of course)

Thanks for the extra detail in the work here Max 👍


Separately and just out of interest (incase you are interested also) in regards to Adam's comment you have left to address, I implemented an improved guard for SSR over in my old suggestion PR over here https://github.com/Kong/kongponents/pull/3045/changes#diff-2adca4150f4c28f9c8c391a0ed2520d130301583843a5bb1890e526f835a8190R106-R113

Obvs we aren't doing that here, but I wanted to see what it would like. Its quite nice that you only have to guard in one place which is out of the main body of code at creation time, and then you can totally forget (and not worry about) the fact that you might be running in SSR everywhere else.

I quite like fixing things in a single place at the root and not having to use ? and if statements everywhere in multiple places, but maybe thats a personal opinion thing 🤷

@portikM portikM requested a review from adamdehaven January 13, 2026 14:23
@portikM portikM merged commit 0f949e8 into main Jan 13, 2026
19 of 20 checks passed
@portikM portikM deleted the fix/khcp-18535-toaster-prevent-duplicate-containers-1 branch January 13, 2026 18:27
kongponents-bot pushed a commit that referenced this pull request Jan 13, 2026
## [9.49.5](v9.49.4...v9.49.5) (2026-01-13)

### Bug Fixes

* **ktoaster:** prevent creating duplicate containers [KHCP-18535] ([#3017](#3017)) ([0f949e8](0f949e8))
@kongponents-bot
Copy link
Collaborator

🎉 This PR is included in version 9.49.5 🎉

The release is available on:

Your semantic-release bot 📦🚀

@kongponents-bot
Copy link
Collaborator

Preview package from this PR in consuming application

In consuming application project install preview version of kongponents generated by this PR:

@kong/kongponents@pr-3017

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants