miso

Thinking in miso

Step 1: Design the model

The model is the single source of truth. The goal is to make it minimal: store what the UI cannot recompute, and compute everything else inside view.

List everything the UI shows

  • The list of bookmarks from the server
  • The search text the user typed
  • The selected tag filter
  • The bookmarks that match the search and the filter
  • The count next to each tag
  • Which bookmark is expanded in the detail pane
  • Whether we are still loading, or something failed

Now ask three questions of each item. Does it change over time? Can it be computed from something else? Is it passed in from a parent?

  • The matching bookmarks and the tag counts are computed from the list, the query and the filter — not state.
  • Loading and failure are states of the list itself, so model them as one type rather than three booleans.

Write it down

data Model = Model
  { _bookmarks :: Remote [Bookmark]   -- what the server said (or hasn't yet)
  , _query     :: MisoString          -- search text
  , _tagFilter :: Maybe Tag           -- Nothing = all
  , _selected  :: Maybe BookmarkId    -- expanded row
  } deriving (Eq, Generic)

data Remote a = Loading | Failed MisoString | Loaded a
  deriving (Eq, Generic)

data Bookmark = Bookmark
  { bookmarkId :: BookmarkId, title :: MisoString, url :: MisoString, tags :: [Tag] }
  deriving (Eq, Generic, FromJSON)

makeLenses ''Model

Four fields. Everything the mockup shows can be produced from them:

visible :: Model -> [Bookmark]
visible m =
  [ b | Loaded bs <- [m ^. bookmarks], b <- bs
      , matches (m ^. query) b
      , maybe True (`elem` tags b) (m ^. tagFilter) ]

tagCounts :: [Bookmark] -> [(Tag, Int)]
tagCounts = ...

Sum types beat booleans

Remote makes the impossible states ("loaded and failed") unrepresentable, and view is forced to handle every case with a case expression. The same idea applies to modes (Viewing | Editing Draft), wizards (one constructor per step) and forms (Either Errors Valid).

Eq is a feature

Every model needs an Eq instance. Derive it. It is what lets miso skip a render when nothing changed, and it is what makes tests trivial: update is a pure RWS so runEffect gives you the new model to compare against an expected value.

Next: break the UI into views and components.