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
      :: model
      -> View context props 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 props parentModel parentAction
key +> comp = VComp (SomeComponent (Just (toKey key)) () comp)

Practically, using this combinator looks like:

viewModel
  :: Int
  -> View context props 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
      , _entries :: [MisoString]
      } deriving (Show, Eq)
    
    mounted :: Lens ParentModel Bool
    mounted = lens _mounted $ \p x -> p { _mounted = x }
    
    entries :: Lens ParentModel [MisoString]
    entries = lens _entries $ \p x -> p { _entries = x }
    
    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 -> mounted %= not
          ChildSaid s -> entries %= take 6 . (s :)
          BadMail _   -> pure ()
    
        view m =
          H.div_ []
            [ H.button_ [ HE.onClick ToggleChild ]
                [ text $ if m ^. mounted
                    then "Unmount the clock"
                    else "Mount the clock"
                ]
            , if m ^. mounted then "clock" +> clock else "no clock"
            , H.ul_ [ HP.class_ "log" ]
                [ H.li_ [] [ text e ] | e <- m ^. entries ]
            ]
    
    -- 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.