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 Int
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 Model Action
followSub _ = when mts $ eachFrame $ \_ts -> do
offset <- readMainThreadRef dragRef
setStylePropertyTransform card
[ CSS.translateX (CSS.px offset) ]
readMainThreadRef dragging
-- keep looping while a drag is active