miso
Copyright(C) 2016-2026 David M. Johnson
LicenseBSD3-style (see the file LICENSE)
MaintainerDavid M. Johnson <code@dmj.io>
Stabilityexperimental
Portabilitynon-portable
Safe HaskellNone
LanguageHaskell2010

Miso.Native

Description

miso native 📱

Miso.Native targets native mobile devices by driving the Lynx runtime instead of the browser DOM. The same MVU programming model, Component API, event delegation and virtual-DOM diffing you use on the web (Miso) carry over unchanged — only the element vocabulary differs (view_, text_, … instead of div_ / span_) and rendering is performed by Lynx's element PAPI rather than by mutating a browser DOM.

This module is the native analog of the miso / startApp entrypoints: native (and nativeWithContext) boot a root Component onto the Lynx runtime.

Enabling native

The native backend is gated behind the native cabal flag. It must be enabled to bring Miso.Native and the Miso.Native.* element / event / FFI modules into scope (build with -fnative). Web / WASM builds are unaffected — all cross-thread machinery lives behind the 1 CPP guard.

The dual-thread architecture

Lynx runs your application across two threads, and miso maps onto both:

  • BTS — the background thread ("background thread script"). This is where your application logic lives. Everything runs here by default: the update function, event handling, Effect scheduling and all virtual-DOM diffing.
  • MTS — the main thread ("main thread script"). This thread owns the actual element tree and rendering. It is where the pixels land. It is also available as a low-latency escape hatch for performance-critical event handling (see Main-thread events below).

The same Haskell bundle runs on both threads; the native runtime (ts/miso-native.ts) selects the BTS or MTS drawing context per-thread from a global flag, so there is no renderer to register — native starts the app directly.

The guiding principle: everything originates on the BTS. The MTS is a rendering surface that the BTS drives across the thread boundary.

Knowing which thread you are on

Lynx builds the bundle with rspeedy — its Rust-based tooling — which compiles the sources twice, once per thread, inlining a compile-time constant (BACKGROUND) that distinguishes the two. That constant surfaces in Haskell as three top-level Bools in Miso.Runtime:

  • mtsTrue when this execution context is the Lynx main thread.
  • btsTrue when this context is the Lynx background thread.
  • webTrue for a plain web / WASM build (neither Lynx thread).

Exactly one is True, and the value is invariant for the lifetime of a JS context, so the runtime computes it once and caches it. Runtime code branches on mts / bts to decide where work runs (e.g. the scheduler suppresses the paint step on the MTS, which keeps only a read-only model replica).

What crosses the thread boundary, and how

Because logic (BTS) and rendering (MTS) live on different threads, miso synchronizes them by shipping messages across the boundary. This is largely invisible, but understanding it explains the API constraints below.

  • Initial draw — The very first Draw happens on the MTS itself, and it does not rely on the BTS diffing a tree and transferring patches across the boundary. The root Component is booted from a StaticPtr (via native / nativeWithContext), so the MTS reconstructs it from the pointer's StaticKey alone and renders the first frame locally (Lynx's instant first frame). Only after this initial draw does the cross-thread patch protocol take over: __every subsequent diff runs on the BTS and ships patches to the MTS__ to apply.
  • Subsequent component mounts — When the BTS view mounts a child Component, that mount is synchronized to the MTS asynchronously using static mounting: the child is wrapped in a static pointer (-XStaticPointers) so only its StaticKey — not a closure — needs to cross the boundary. The MTS dereferences the key to rebuild the component locally. See vcomp / mountStatic_.
  • State synchronization — The BTS owns the shared model and ships it to the MTS as it changes (JSON-serialized, hence the ToJSON / FromJSON constraints on native mounting combinators), so main-thread *MainWith handlers observe an eventually-consistent copy. A child's initial props ride the static mount payload — the static pointer carries the constructor and the props value is shipped separately, so it may depend on the parent model. But props and the global context are not re-synced on later changes: after the first frame they stay background-thread-only (matching ReactLynx — see Main-thread events).
  • Events — Events raised on the MTS are, by default, forwarded to the BTS where update runs (see below). Cross-thread handlers are carried as an EventHandler, embedded with event . static (…) so the peer thread can rebuild the handler from its StaticKey.

First-frame rendering (instant first frame)

The MTS painting frame one itself (the Initial draw above) is Lynx's instant first frame: the user sees UI without waiting for a background render and patch round-trip. Meanwhile the BTS boots the same root and builds the identical virtual-DOM tree in lockstep — with __deterministic nodeId parity, so both threads address the same elements — but suppresses its own create-patches__ for that first frame, since the MTS already painted them. A single global initialDraw latch governs this on both threads; native / nativeWithContext clears it once the whole root mount has finished.

After that handover the responsibilities are fixed, mirroring ReactLynx: the BTS is the sole diff / paint authority — it runs update, diffs, and ships patches — while the MTS only applies those patches (and runs main-thread scripts / handlers). The MTS never diffs or repaints from the scheduler again; this is why the shared model is BTS-owned and why nothing you do on the MTS should try to redraw declaratively.

Static mounting

Because component constructors, event handlers and effects may need to be reconstructed on the other thread, native miso threads them across the boundary as static pointers rather than closures. This requires the -XStaticPointers language extension.

The root component is mounted with mountStatic_ wrapped in static:

{-# LANGUAGE StaticPointers #-}
-----------------------------------------------------------------------------
module Main where
-----------------------------------------------------------------------------
import Miso
import Miso.Native
-----------------------------------------------------------------------------
main :: IO ()
main = native nativeEvents (static (mountStatic_ app))

Child components are embedded in a view the same way, with vcomp:

view _ _ _ = view_ [] [ vcomp () (static (mountStatic_ childComponent)) ]

Static-pointer limitation. A static form may only close over top-level, closed bindings — it cannot capture local variables. This is why component constructors and main-thread handlers are supplied as references to top-level definitions, with any runtime data (props, decoded event payloads) shipped separately as serialized values rather than captured in a closure.

Effects: choosing a thread

Because an IO closure can't cross the thread boundary (only JSON-serialized actions can), cross-thread work is expressed as dispatching an action to the thread that should handle it. Two combinators do this:

  • runOnBG action — run action's update on the background thread (BTS). Used by a main-thread event handler that needs to change shared state, since the BTS solely owns the model.
  • runOnMain action — run action's update on the main thread (MTS). Used by a BTS effect that needs an imperative main-thread operation (see Miso.Native.MainThread).

Each ships only the given action to the target thread (or dispatches it locally when already there), where its update runs exactly once. Sibling effects in the current update are unaffected, and nothing is double-executed. Off the native runtime both are an ordinary local dispatch, equivalent to issue.

Subscriptions and threads

A Sub is dynamic — it is just a Sink action -> IO () run in a forked thread — and a component's subs are started on __every thread it mounts on. So a Sub runs on both the BTS and the MTS__ (once each), and each copy dispatches into its own thread's scheduler.

Because a Sub is ordinary runtime IO — unlike a static event handler, whose thread is fixed at compile time — it selects its own thread at runtime with the mts / bts Bools. This is the dynamic analog of a handler's *Main variant:

-- background-only: open the socket once, feed the model
wsSub sink = when bts (websocketConnect "wss://…" sink)

-- main-thread-only: drive an imperative animation
animSub _ = when mts (eachFrame step)

Guard anything that must be single-owned. Without a bts / mts gate a stateful sub double-runs — two websocket connections, a timer ticking on both threads — so pin such subs to one thread. The no-op fork on the other thread returns immediately.

Main-thread events

Thread affinity is per-handler, not per-event-name. Any given event can be handled on either thread; the choice is made at each handler, so the same event (say tap) may run on the BTS for one element and the MTS for another. The default is the BTS — a plain onTap handler runs on the background thread. Opting a handler into the MTS is explicit (the *Main variants below); nothing runs on the main thread unless you ask for it.

By default an event handler runs on the BTS: the event is forwarded from the MTS, update runs on the BTS, the model changes, and the resulting diff is shipped back to the MTS to paint. That round-trip is fine for most interactions but adds latency for gesture- and scroll-linked animation.

For those cases, handlers have *Main-suffixed variants (e.g. onTapMain, onTouchMoveMain) that run __synchronously on the MTS__ — no VDOM diff, no patches, no BTS round-trip. Such a handler is imperative: it mutates the target element directly through the helpers in Miso.Native.MainThread (e.g. setStyleProperty). The *MainWith variants additionally hand the handler the current model and the target DOMRef (\event model domRef -> action).

Because a main-thread handler must be reconstructed on the MTS, it is an EventHandler embedded with event . static — so main-thread event handlers require -XStaticPointers (the static keyword is how the handler crosses to the MTS by StaticKey):

{-# LANGUAGE StaticPointers #-}

view _ _ _ =
  view_ [ event (static (onTapMain HandleTap)) ] []

The same static capture limitation applies: an onTapMain handler refers to a top-level action / function; runtime data reaches the handler via the decoded event payload, not a captured closure.

The generic primitives (on / onMain). The per-element on* / on*Main helpers are sugar over two combinators, and the same (eventName, decoder, toAction) works with either — that is how one event is captured on whichever thread you choose, per handler:

  • on name decoder toAction → a plain Attribute that runs on the BTS. No static: a background handler is reconstructed nowhere else, so it may close over the enclosing view.
  • onMain name decoder toAction → an EventHandler that runs on the MTS, embedded with event . static.
-- same tap event, one handler per thread:
view_ [ on "tap" emptyDecoder (\_ _ _ -> Grow) ] children                     -- BTS
view_ [ event (static (onMain "tap" emptyDecoder onTapMain)) ] children  -- MTS

The Attribute-versus-EventHandler+static split is the mechanism: only the main-thread handler has to cross to the MTS by StaticKey, which is why onMain (and every *Main helper) needs -XStaticPointers while on does not. (onMainWithOptions exposes Phase / Options for the MTS variant, mirroring onWithOptions.)

Reaching the model (and why it is passed, not captured). A static main-thread handler cannot close over the model, props or context from the enclosing view — those are local bindings, which static forbids. So rather than capture them, the *MainWith variants __pass the model as an argument__ to the handler, giving imperative MTS code the state it needs to integrate without a BTS round-trip. Note this is the main-thread's own copy of the model: it is populated on the MTS eventually consistently from the BTS (the authoritative model still lives on the background thread), so a handler may observe a value slightly behind the latest BTS state.

Props and context are not on the main thread. Unlike the model, a component's props and the app-global context are not mirrored to the MTS at all (matching ReactLynx, where React state — and therefore props and context — is background-thread-only). They live solely on the BTS; the MTS keeps only its boot values, so getProps / getContext inside a main-thread handler would read stale data. Only the model is hydrated to the MTS (eventually consistently, as above). If a main-thread handler needs a prop or context value, fold it into the model or carry it in the dispatched action payload — do not read props or context on the main thread. This also means less cross-thread traffic: the BTS ships a props/context change to the MTS only via the initial MOUNT (for props), never on every subsequent change.

Ownership caveat. A property you drive imperatively from the MTS must not also be written declaratively by the BTS view for the same element: both threads write the shared element tree through the same PAPI with no arbitration, so one will clobber the other. Keep a single owner per (element, property) — typically compositor properties like transform / opacity that the view leaves alone.

Main-thread-local state: MainThreadRef

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 analog 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 (.=, %=, +=, …).

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.

Platform APIs and thread restrictions

Mirroring Lynx ("not all APIs exist on both threads"), miso's native APIs are split by thread, and calling one from the wrong thread fails at runtime — the type system does not catch it, so guard with mts / bts when code may run on either thread. Neither module is re-exported here; import it directly.

update runs on the BTS by default, so this just works there; from a main-thread handler, hop to the BTS first with runOnBG. On the MTS the module is undefined and the call logs a consoleError.

  • Main-thread element ops (MTS-only) — the imperative helpers in Miso.Native.MainThread (setStyleProperty etc.) and the element PAPI they call exist only on the MTS; on the BTS they no-op. Drive them from a *Main handler or via runOnMain.

A minimal native component

-----------------------------------------------------------------------------
{-# LANGUAGE StaticPointers #-}
-----------------------------------------------------------------------------
import Miso
import Miso.Native
-----------------------------------------------------------------------------
view :: context -> props -> Model -> View context Action
view _ _ m =
  vfrag_
  [ view_ [ onTap Increment ] [ text_ [] [ "+" ] ]
  , text_ [] [ text $ ms (show m) ]
  , view_ [ onTap Decrement ] [ text_ [] [ "-" ] ]
  ]

More information on how to use miso is available on GitHub

http://github.com/dmjio/miso

Synopsis

Entrypoint

native :: Events -> StaticPtr (SomeStaticComponent () ()) -> IO () Source #

The native drawing context is already selected per-thread by the runtime (ts/miso-native.ts picks bts or mts from BACKGROUND), so there is no renderer to register — we start the app directly.

{-# LANGUAGE StaticPointers #-}

import Miso
import Miso.Native

main :: IO ()
main = native nativeEvents (static (mountStatic_ app))

nativeWithContext :: (ToJSON context, FromJSON context, Eq context) => Events -> context -> StaticPtr (SomeStaticComponent () context) -> IO () Source #

Like native, but the user can specify a global context object.

{-# LANGUAGE StaticPointers #-}

import Miso
import Miso.Native

main :: IO ()
main = nativeWithContext nativeEvents () (static (mountStatic_ app))

Component mounting

mountStatic_ Source #

Arguments

:: (Eq context, Eq model, FromJSON model, ToJSON model, FromJSON action, ToJSON action, FromJSON context, ToJSON context) 
=> Component context () model action

Component to mount

-> SomeStaticComponent () context 

Component mounting combinator.

Note: only use this if you're certain you won't be diffing two Component against each other. Otherwise, you will need a key to distinguish between the two Component, to ensure unmounting and mounting occurs.

mountStatic_ $ component model noop $ \m ->
 div_ [ id_ "foo" ] [ text (ms m) ]

Since: 1.9.0.0

mountStaticWithProps Source #

Arguments

:: (Eq context, Eq props, Eq model, FromJSON model, ToJSON model, FromJSON action, ToJSON action, FromJSON props, ToJSON props, FromJSON context, ToJSON context) 
=> Component context props model action

Component to mount

-> SomeStaticComponent props context 

Component mounting combinator.

Note: only use this if you're certain you won't be diffing two Component against each other. Otherwise, you will need a key to distinguish between the two Component, to ensure unmounting and mounting occurs.

It takes only the component and yields a closed props -> component constructor (SomeStaticComponent) suitable for static — the props value is not supplied here, but later at the vcomp site, so a parent can pass runtime props (e.g. derived from its own model) without an explicit lambda:

Static mounting automatically provides the key_ at compile time (via staticKey). So the user doesn't need to use the +> combinators.

vcomp (model ^. field) (static (mountStaticWithProps child))

Since: 1.11.0.0

mountStaticUseContext Source #

Arguments

:: (Eq context, Eq model, FromJSON model, ToJSON model, FromJSON action, ToJSON action, FromJSON context, ToJSON context) 
=> Component context () model action

Component to mount

-> SomeStaticComponent () context 

Static Component mounting combinator that opts the child into app-global React-style context updates.

Equivalent to mountStatic_, but sets useContext = True on the mounted Component so it re-renders whenever the context changes (see modifyContext). The static-key counterpart of mountUseContext — use this (with vcomp_) instead of mountUseContext under the Lynx dual-thread (1) backend.

vcomp_ (static (mountStaticUseContext myComp))

Since: 1.12.0.0

Element

FFI

Event