-
Notifications
You must be signed in to change notification settings - Fork 217
Description
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.disposeHere's (roughly) what happens next.
runUIcreates a new render spec with references toHTMLDocumentand the root node for the application to run in. In this example, that's the<body>element.- The component is run for the first time, after which the user is returned a
HalogenIOrecord they can use to call adisposefunction to clean up subscriptions and finalizers and remove the component from the DOM. - Each time the
renderfunction runs theDriverStatesaves the resulting VDom tree in a field calledrendering - When the
disposefunction is called, thisrenderingfield 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
}
}