Reacting to external events with miso subscriptions
A subscription lets a Component react to events that originate outside the update function — browser events, timers, WebSockets, or any other IO source.
-- | A long-running operation that can write to a Component's event queue.type Sub action = Sink action -> IO ()-- | A function that writes an action to the global event queue.type Sink action = action -> IO ()
A Sub is a function that receives a Sink and performs some long-running IO. Whenever it wants to update the application model, it calls the sink with an action value. That action is then processed by the update function on the next scheduler tick.
-- | Fires on every popstate event with the current URI.uriSub :: (URI -> action) -> Sub action-- | Like uriSub, but parses the URI through a Router instance.routerSub :: Router route => (Either RoutingError route -> action) -> Sub action
-- | Fires with the set of currently pressed key codes.keyboardSub :: (IntSet -> action) -> Sub action-- | Fires with an Arrows value derived from arrow key codes.arrowsSub :: (Arrows -> action) -> Sub action-- | Like arrowsSub, but uses W/A/S/D key codes.wasdSub :: (Arrows -> action) -> Sub action
The Arrows type encodes directional intent as a pair of Int values in {-1, 0, 1}:
data Arrows = Arrows { arrowX :: !Int -- -1 left, 0 neutral, 1 right , arrowY :: !Int -- -1 down, 0 neutral, 1 up }
data Action = ArrowsChanged Arrowssubs :: [Sub Action]subs = [ arrowsSub ArrowsChanged ]update :: Action -> Effect parent Model Actionupdate = \case ArrowsChanged arrows -> modify $ \m -> m { playerX = playerX m + arrowX arrows , playerY = playerY m + arrowY arrows }
keyboardSub tracks a set of simultaneously pressed keys and clears it automatically when the window loses focus, so keys never get “stuck”.
-- | Subscribe to any window-level event by name, with a custom Decoder.windowSub :: MisoString -- ^ Event name (e.g. "resize", "scroll") -> Decoder r -- ^ Decoder for the event payload -> (r -> action) -> Sub action-- | Subscribe to pointermove events, delivering Coord values (client x/y).windowCoordsSub :: (Coord -> action) -> Sub action-- | Subscribe to pointermove events, delivering PointerEvent values.windowPointerMoveSub :: (PointerEvent -> action) -> Sub action-- | Like windowSub but with custom stopPropagation / preventDefault options.windowSubWithOptions :: Options -> MisoString -> Decoder r -> (r -> action) -> Sub action
The pattern follows bracket: acquire registers listeners and returns a handle; release receives that handle and unregisters the listeners. The miso runtime calls release automatically when the component unmounts.Here is the onLineSub implementation from the source, which shows the pattern:
onLineSub :: (Bool -> action) -> Sub actiononLineSub f sink = createSub acquire release sink where release (cb1, cb2) = do FFI.windowRemoveEventListener "online" cb1 FFI.windowRemoveEventListener "offline" cb2 acquire = do cb1 <- FFI.windowAddEventListener "online" (\_ -> sink (f True)) cb2 <- FFI.windowAddEventListener "offline" (\_ -> sink (f False)) pure (cb1, cb2)
For a timer-based example that does not use createSub:
import Control.Concurrent (threadDelay)import Control.Monad (forever)data Action = TicktimerSub :: Sub ActiontimerSub sink = forever $ do threadDelay 1000000 -- 1 second sink Tickapp :: App Model Actionapp = (component initialModel update view) { subs = [ timerSub ] }
Custom subscriptions run in their own thread. If you use forever, ensure that any cleanup (file handles, network connections) is handled via createSub so it runs when the component unmounts. A bare forever loop will continue until the process exits.