miso

Thinking in miso

Step 3: Actions and update

An action is something that happened: the user typed, the server answered, a timer fired. Name actions after events, not after setters, and let update decide what they mean.

The vocabulary

data Action
  = Init                              -- mounted: go fetch
  | GotBookmarks (Either MisoString [Bookmark])
  | QueryChanged MisoString
  | TagPicked (Maybe Tag)
  | Select BookmarkId
  | BookmarkAdded Bookmark            -- mailed up by the form
  deriving (Eq, Show)

QueryChanged rather than SetQuery: the name leaves update free to also reset the selection, or later to debounce a request, without renaming anything.

update is a fold

update :: Action -> Effect Ctx () Model Action
update = \case
  Init ->
    getJSON "/api/bookmarks" [] (GotBookmarks . Right) (GotBookmarks . Left . ms)

  GotBookmarks (Right bs) -> bookmarks .= Loaded bs
  GotBookmarks (Left err) -> bookmarks .= Failed err

  QueryChanged q -> do
    query    .= q
    selected .= Nothing                    -- a new search deselects

  TagPicked t   -> tagFilter .= t
  Select bid    -> selected  %= toggle bid

  BookmarkAdded b -> bookmarks %= fmap (b :)

Every branch is a small, total function on the model. There is no await, no promise chain, no setState callback ordering: getJSON schedules the request and the answer arrives later as GotBookmarks, which is handled like any other action.

IO lives at the edge

  • io / io_ for asynchronous work — the default. The scheduler runs it off the update thread and catches exceptions.
  • sync only for cheap, must-be-ordered reads (a localStorage lookup, measuring an element).
  • mount = Just Init to kick things off when the component appears; unmount to clean up.
  • Long-lived sources are subscriptions — a websocket, rAFSub, routerSub — not effects.

Because subs can be started and stopped from update, a debounce is a tiny sub rather than a library:

QueryChanged q -> do
  query .= q
  stopSub "debounce"
  startSub "debounce" $ \sink -> do
    threadDelay 250000
    sink (Search q)

Testing

update is an RWS and view is a function, so both are testable without a browser: run update on a model, compare the resulting model with ==, and render view to HTML with toHtml to assert on markup.

Next: connect the pieces and ship.