thomashoneyman/purescript-halogen-hooks

useTickEffect finalizers don't get run when dependencies change

JordanMartinez opened this issue · 1 comments

Given this code:

module Test.UseTickEffect where

import Prelude

import Data.Maybe (Maybe(..))
import Data.Symbol (SProxy(..))
import Data.Tuple.Nested ((/\))
import Effect.Class (class MonadEffect)
import Effect.Class.Console (log)
import Halogen (liftEffect)
import Halogen as H
import Halogen.HTML as HH
import Halogen.HTML.Events as HE
import Halogen.Hooks (Hooked)
import Halogen.Hooks as Hooks

sproxy :: SProxy "useTickEffect"
sproxy = SProxy

component :: forall q o m. MonadEffect m => H.Component HH.HTML q Unit o m
component = Hooks.component \_ -> hook

hook :: forall slots output m hooks. MonadEffect m
     => Hooked slots output m hooks (Hooks.UseEffect (Hooks.UseState Int hooks)) _
hook = Hooks.do
  state /\ tState <- Hooks.useState 0
  Hooks.captures { state } Hooks.useTickEffect do
    liftEffect $ log $
      "useTickEffect: This message appears in two situations. First, when the \
      \component is initialized. Second, every time the state value changes, \
      \but it appears only AFTER the cleanup message appears."
    pure $ Just do
      liftEffect $ log $
        "useTickEffect: [Cleanup Message]. This message appears in two \
        \situations. First, every time the state value changes. Second, \
        \when component is removed."

  Hooks.pure $
    HH.div_
      [ HH.p_
        [ HH.text $ "Click the button to change the value of the dependency, \
                    \which will trigger the useTickEffect code."
          ]
      , HH.button
        [ HE.onClick \_ -> Just $ Hooks.modify_ tState (_ + 1) ]
        [ HH.text $ "Trigger the `useTickEffect` code by \
                    \increasing the state value by 1"
        ]
      ]

The finalizer doesn't get run when the state changes.

The issue is that the tick effect's finalizers are stored in the same array as a lifecycle effects' finalizers. Thus, when the dependency changes, none of the tick finalizers can be run without also running the lifecycle finalizers.

My initial thought for solving this problem was to change the type signature for the effectCells row to store both the MemoValues and the Finalizer. This was inspired by how memoCells stores both the MemoValues and MemoValue:

type InternalHookState q i ps o m =
  { -- ...

    -- inspiration from memoCells
  , memoCells :: QueueState (MemoValues /\ MemoValue) -- the memo values via useMemo

  -- before: stores only memo values
  , effectCells :: QueueState MemoValues
  -- after: stores memo values and the finalizer to run when memos changes
  , effectCells :: QueueState (MemoValues /\ HookM ps o m Unit)
  }

When I tried to do this, I ran into a problem. At some point, this code is called:

newQueue = unsafeSetCell index (memos /\ finalizer) queue

This is problematic because I don't have a reference to the new finalizer at this point. For context, a finalizer gets created when we initially run the effect. In some situations, we may wish to change what that finalizer is, so we cannot reuse the finalizer we created when we originally initialized it:

mbFinalizer <- evalHookM (interpretUseHookFn Queued hookFn) act

During the Step case, we enqueue the above effect and run it later. It is during this evaluation (I believe) where the initial effect gets rerun, which produces the new finalizer:

let eval = void $ evalHookM (runUseHookFn Queued hookFn) act
modifyState_ \st -> st { evalQueue = Array.snoc st.evalQueue eval }

I'm not sure what would be the best way to get this new finalizer.