Skip to content

Disposal fails if component root element changes #675

@thomashoneyman

Description

@thomashoneyman

Issue

If you render a component with a root HTML element (like HH.div_ ...) and, on a subsequent render, change that node to something else (like HH.section_ ...) then your component can no longer be disposed of. This happens because the call to dispose uses the element rendered on the first component render, and will attempt to remove that node even if the component's root element has changed since then.

Context

When you use Halogen.VDom.Driver.runUI to run your root component you receive a HalogenIO record in return, which among other things lets you dispose of your application -- clean up subscriptions, run finalizers, and remove it from the DOM.

The usual code looks something like this:

    body <- awaitBody
    io <- runUI component unit body
    -- ... do something ...
    io.dispose

Here's (roughly) what happens next.

  1. runUI creates a new render spec with references to HTMLDocument and the root node for the application to run in. In this example, that's the <body> element.
  2. The component is run for the first time, after which the user is returned a HalogenIO record they can use to call a dispose function to clean up subscriptions and finalizers and remove the component from the DOM.
  3. Each time the render function runs the DriverState saves the resulting VDom tree in a field called rendering
  4. When the dispose function is called, this rendering field is retrieved and the rendered code is removed.

However, if the root element has changed from the first render then the disposal will fail. It will attempt to remove a tree of HTML that no longer exists. You can verify this by using the reproduction example below and adding a Debug.Trace.spy statement to this line:

unDriverStateX (traverse_ renderSpec.dispose <<< _.rendering) dsx

If you render a h1 as the root, for example, and then change it to h2, then you'll see that the driver state still sees h1 even though that's no longer true.

Reproduction:

This code is available in a gist runnable on Try PureScript:
https://try.purescript.org/?gist=4c7f24a18ac9120b715c95b0b769d3f9

Expand to see code inline...
module Main where

import Prelude

import Control.Parallel (parTraverse_)
import Data.Maybe (Maybe(..))
import Effect (Effect)
import Effect.Aff (Aff, Milliseconds(..), delay)
import Halogen as H
import Halogen.Aff (awaitBody, runHalogenAff)
import Halogen.HTML as HH
import Halogen.VDom.Driver (runUI)

main :: Effect Unit
main = runHalogenAff do
  body <- awaitBody
  [ renderGood, renderBad1, renderBad2 ] # parTraverse_ \render -> do
    loadingIO <- runUI (loading render) unit body
    delay (Milliseconds 2000.0)
    loadingIO.dispose

-- This works because the root node (`div_`) never changes.
renderGood :: forall w i. Boolean -> HH.HTML w i
renderGood =
  if _ then 
    HH.div_ [ HH.h1_ [ HH.text "Loading" ] ] 
  else 
    HH.div_ [ ]

-- This fails because the root node changes
renderBad1 :: forall w i. Boolean -> HH.HTML w i
renderBad1 = 
  if _ then 
    HH.h1_ [ HH.text "Loading" ] 
  else 
    HH.text ""

-- This fails because the root node changes
renderBad2 :: forall w i. Boolean -> HH.HTML w i
renderBad2 =
  if _ then
    HH.h1_ [ HH.text "Loading" ]
  else
    HH.h2_ [ HH.text "Loading" ]

-- This component starts rendering the `true` branch, and after 
-- 500 milliseconds renders the `false` branch
loading 
  :: forall q i o
   . (Boolean -> H.ComponentHTML Unit () Aff)
  -> H.Component HH.HTML q i o Aff
loading render =
  H.mkComponent
    { initialState: \_ -> false
    , render
    , eval: H.mkEval $ H.defaultEval 
        { initialize = Just unit
        , handleAction = \_ -> H.liftAff (delay (Milliseconds 500.0)) *> H.put true 
        }
    }

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions