miso

Native (mobile)

Main-thread-local state

A main-thread handler is imperative and must not write the BTS-owned model: shared state changes belong on the background thread, so dispatch them with runOnBG. But gestures and scroll-linked animation often need mutable state that lives only on the MTS — the current drag offset, a fling velocity, whether a follow loop is active. For that, use a MainThreadRef, a thin IORef wrapper for main-thread-only state (the analogue of ReactLynx's MainThreadRef):

dragRef :: MainThreadRef Double
dragRef = mainThreadRef 0
{-# NOINLINE dragRef #-}

mainThreadRef allocates the underlying cell as a CAF via unsafePerformIO, so every top-level binding needs its own NOINLINE pragma — otherwise GHC may inline the CAF and split the state into independent copies. Reads and writes (readMainThreadRef / writeMainThreadRef / modifyMainThreadRef) are ordinary IORef operations — safe without atomics because the MTS is single-threaded — and modifyMainThreadRef_ takes a State a () so you can drive updates with the Miso.Lens operators.

eachFrame

It pairs with eachFrame for a vsync-coalesced animation loop: read the latest gesture state from the ref, imperatively paint at most once per frame (via setStyleProperty / setStylePropertyTransform), and stop by returning False when the gesture ends.

followSub :: Sub Action
followSub _ = when mts $ eachFrame $ do
  offset <- readMainThreadRef dragRef
  setStylePropertyTransform card ("translateX(" <> ms offset <> "px)")
  readMainThreadRef dragging   -- keep looping while a drag is active