miso

Core concepts

Components

A Component bundles a model, the update that evolves it and the view that renders it — plus everything around the edges: subscriptions, lifecycle hooks, a mailbox. Components nest, forming a typed UI tree.

The record

The component smart constructor fills in sane defaults; you override fields with record update syntax:

data Component context props model action = Component
  { model           :: model                        -- initial model
  , hydrateModel    :: Maybe (IO model)             -- optional model to hydrate from (SSR)
  , update          :: action -> Effect context props model action
  , view            :: context -> props -> model -> View context model action
  , useContext      :: Bool                         -- re-render when the global context changes
  , subs            :: [ Sub action ]               -- long-running subscriptions
  , styles          :: [ CSS ]                      -- dev only: append <style>/<link> to <head>
  , scripts         :: [ JS ]                       -- dev only: append <script> to <head>
  , mountPoint      :: Maybe MountPoint             -- defaults to <body>
  , logLevel        :: LogLevel                     -- Off | DebugHydrate | DebugEvents | DebugAll
  , mailbox         :: Value -> Maybe action        -- receive mail from other components
  , eventPropagation :: Bool                        -- let events bubble past this component
  , mount           :: Maybe action                 -- action dispatched on mount
  , unmount         :: Maybe action                 -- action dispatched on unmount
  , onPropsChanged  :: Maybe (props -> props -> action)
  }

Composition

Components can contain other components. This is accomplished through the mounting combinator (+>), which encodes a typed component hierarchy. All components in a tree share the same global context type.

(+>)
  :: (Eq context, Eq model)
  => MisoString
  -> Component context () model action
  -> View context parentModel parentAction
key +> comp = VComp (SomeComponent (Just (toKey key)) () comp)

Practically, using this combinator looks like:

viewModel :: context -> props -> Int -> View context Int Action
viewModel _ _ _ =
  H.div_ [ HP.id_ "container" ] [ "counter" +> counter ]

The "counter" string is a unique Key that identifies the component at runtime. Keys matter when diffing two components: when intentionally replacing a component it is important to specify a new key, otherwise the old one will not be unmounted.

It is possible to mount a component with mount_, which avoids specifying a key, but this should only be used when you are certain the component will never be diffed against another component. When in doubt, use (+>) and key your component. To pass props use mountWithProps_ (keyed) or mountWithProps.

Lifecycle hooks

Components are mounted during diffing. All components are equipped with mount and unmount hooks, allowing custom actions to be dispatched in response to lifecycle events:

child :: Component ctx () Model Action
child = (component m u v)
  { mount   = Just Connect
  , unmount = Just Disconnect
  }

Element nodes have their own hooks (onCreated, onDestroyed, …) — see the View DSL.

mount, unmount, subs and the mailboxlive

⏱ 0s since mount

    -- A parent that mounts and unmounts a keyed child, and logs what the child
    -- reports through the mailbox.
    data ParentModel = ParentModel { _mounted :: Bool, _events :: [MisoString] }
      deriving (Show, Eq)
    
    data ParentAction
      = ToggleChild
      | ChildSaid MisoString
      | BadMail MisoString
    
    parent :: Eq ctx => Component ctx () ParentModel ParentAction
    parent = (component (ParentModel True []) update view)
      { mailbox = checkMail ChildSaid BadMail }   -- receive mail from the child
      where
        update = \case
          ToggleChild -> modify (\m -> m { _mounted = not (_mounted m) })
          ChildSaid s -> modify (\m -> m { _events = take 6 (s : _events m) })
          BadMail _   -> pure ()
    
        view _ () m =
          H.div_ []
            [ H.button_ [ HE.onClick ToggleChild ]
                [ text (if _mounted m then "Unmount the clock" else "Mount the clock") ]
            , if _mounted m then "clock" +> clock else "no clock"
            , H.ul_ [ HP.class_ "log" ] [ H.li_ [] [ text e ] | e <- _events m ]
            ]
    
    -- The child: a clock that ticks from a subscription and reports its
    -- lifecycle to the parent.
    data ClockAction = Tick | Mounted | Unmounted
    
    clock :: Component ctx () Int ClockAction
    clock = (component 0 update view)
      { mount   = Just Mounted            -- dispatched when the component appears
      , unmount = Just Unmounted          -- ...and when it goes away
      , subs    = [ everySecond ]         -- runs for the component's lifetime
      }
      where
        everySecond sink = forever (threadDelay 1000000 >> sink Tick)
    
        update = \case
          Tick      -> this += 1
          Mounted   -> mailParent ("clock mounted" :: MisoString)
          Unmounted -> mailParent ("clock unmounted" :: MisoString)
    
        view _ () secs = H.p_ [] [ "⏱ ", text (ms secs), "s since mount" ]

    Toggle the clock: the child's mount and unmount actions run, its subs start and stop with it, and it reports back through the parent's mailbox. Note the "clock" +> clock key — that is what makes the diff mount and unmount rather than patch.

    The View type

    The View is a rose tree of nodes, mutually recursive with Component through view:

    data View context model action
      = VNode Namespace Tag [Attribute model action] [View context model action] DirectEvents
      | VText (Maybe Key) MisoString
      | VComp (SomeComponent context)
      | forall props . VCompStatic (StaticPtr (SomeStaticComponent props context)) props
      | VFrag (Maybe Key) [View context model action]
    
    data SomeComponent context
      = forall model action props . (Eq context, Eq model, Eq props)
      => SomeComponent (Maybe Key) props (Component context props model action)

    VNode and VText map one-to-one onto the physical DOM. VComp and VFrag are abstract (they live only in the virtual DOM). The existential SomeComponent is what allows embedding polymorphic components in a View. VCompStatic carries a static pointer to its constructor and is used by the native dual-thread runtime.