Skip to content

Switching dynamic children while remounting the parent element causes previous children to momentarily be activated before being removed #188

@raquo

Description

@raquo

This is caused by raquo/Airstream#145.

Imagine you have:

val child<N> = span(
  "Child <N>",
  onMountUnmountCallback(
    mount = _ => println("mount child <N>"),
    unmount = _ => println("unmount child <N>")
  )
)

val childrenVar = Var(List(child1, child2)) // Var with list of elements

div(
  children <-- childrenVar
)

You mount this div, and it renders the two children, activating their subscriptions. As the mount / unmount hooks are executed, the app prints "mount child 1" and "mount child 2" to the dev console, as expected.

Suppose you then unmount this div. its children will be deactivated, so you will see "unmount child 1" and "unmount child 2" printed in the console, again as expected.

Now, you switch the content of childrenVar – while the div is still unmounted. You set it to List(child3, child4). As expected, nothing happens immediately because the div is unmounted and its subscriptions are inactive. The div still has child1 and child2 elements as its children, as expected.

The problem shows itself when you then re-mount this div. During the mounting process, you expect the div's children <-- childrenVar to pick up the new list of children – child3 and child4 – and immediately render that, discarding the previous children child1 and child2 without ever re-mounting / re-activating them. And that's pretty much what happens... except for that last part. Due to the linked Airstream bug, the removal of child1 and child2 is delayed until the end of the mounting process, and so the child1 and child2 elements are inadvertently activated, so you'll see not only "mount child 3" and "mount child 4" printed in the console, but also "mount child 1" and "mount child 2", immediately followed by "unmount child 1" and "unmount child 2".

This bug only happens when you change the contents of a dynamic receiver like children, child, or onMountInsert while the element is unmounted, and then re-mount that same element. It does not happen during regular data updates while the element is mounted – perhaps why it went unnoticed for so long.

And, visually, you would likely never observe this bug, because all this happens synchronously before the browser gets a chance to actually render anything. But, the subscriptions in child1 and child2 were briefly activated even though they should not have been. We were still correctly shutting down those subscriptions, so there should be no memory leaks, but we definitely should not be "power-cycling" previous elements in this manner. This could cause observable undesirable side effects such as network requests or non-idempotent state updates.

I already fixed the issue in Airstream, and will push the Laminar side of the fix soon, after some more polish. The fix will be released in the upcoming v18.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions