Thinking in miso
Step 2: Views and components
In miso there are two ways to split a UI: view functions (any function returning a View) and components (a Component with its own model, update and lifecycle). Reach for the first by default.
Draw the boxes
BookmarksApp (Component — owns the Model)
├── searchBar (view function: query, onInput)
├── tagSidebar (view function: tag counts, selected tag)
├── bookmarkTable (view function)
│ └── bookmarkRow (view function, keyed by id)
├── detailPane (view function)
└── "add" +> addBookmarkForm (Component — owns its draft, validation, submit state)When is something a Component?
A Component costs a little ceremony (its own model and action type, a key) and buys isolation. Make one when a piece of UI:
- owns state nobody else needs — the add form's draft and validation errors are irrelevant to the table;
- needs subscriptions or lifecycle hooks — a clock, a websocket, a third-party widget initialised in
mount; - is reused with different props — the same
avatarcomponent mounted for each user; - should re-render independently — its model changes often while the parent's does not.
Otherwise write a function. bookmarkRow :: Bookmark -> View ctx Model Action is simpler than a component, is trivially testable, and re-renders as part of its parent.
bookmarkRow :: Maybe BookmarkId -> Bookmark -> View ctx Model Action
bookmarkRow selected b =
H.tr_ [ key_ (bookmarkId b) -- stable identity in the list
, HP.classList_ [ ("selected", selected == Just (bookmarkId b)) ]
, HE.onClick (Select (bookmarkId b)) ]
[ H.td_ [] [ text (title b) ]
, H.td_ [] [ text (MS.intercalate ", " (tags b)) ]
]Where does each piece of state live?
| State | Lives in | Because |
|---|---|---|
| bookmarks, query, filter, selection | BookmarksApp model | several views read it; the app owns it |
| the add form's draft & errors | addBookmarkForm model | private; nobody else cares until submit |
| the currently selected tag, as seen by the form | props | the form only reads it to pre-fill a tag |
| language, theme, current user | context | global; every component may read it |
The rule of thumb: state lives in the closest common owner of everything that reads or writes it. Push it up only as far as it needs to go, and pass it down as props.
viewApp :: Ctx -> () -> Model -> View Ctx Model Action
viewApp ctx _ m =
H.main_ []
[ searchBar (m ^. query)
, H.div_ [ HP.class_ "columns" ]
[ tagSidebar (m ^. tagFilter) (tagCounts (loadedOr [] (m ^. bookmarks)))
, bookmarkTable (m ^. selected) (visible m)
, detailPane (selectedBookmark m)
]
, mountWithProps_ "add-form" (FormProps (m ^. tagFilter)) addBookmarkForm
]