A full-featured TodoMVC implementation demonstrating miso’s capabilities
TodoMVC is a standardized benchmark application used to compare web frameworks. The miso implementation covers the full feature set: creating, completing, editing, and deleting todos, filtering by status, and persisting state across page reloads via local storage.
The application model holds everything needed to render the UI:
data Model = Model { todos :: [Todo] -- the list of todo items , field :: MisoString -- current value of the new-todo input , uid :: Int -- auto-incrementing id for new todos , visibility :: Filter -- which subset of todos to show } deriving (Show, Eq)data Todo = Todo { description :: MisoString -- the todo text , completed :: Bool -- whether it has been checked off , editing :: Bool -- whether the inline editor is open , todoId :: Int -- stable identifier } deriving (Show, Eq)data Filter = All | Active | Completed deriving (Show, Eq)
todos is the authoritative list. All filtering is computed from this list on each render — no secondary derived list is stored in the model.
field tracks the live value of the new-todo <input> element.
uid is incremented each time a todo is added, giving every item a stable identity for the virtual DOM differ.
visibility reflects the current URL fragment (#/, #/active, #/completed).
Every user interaction is expressed as a constructor of Action:
data Action = NoOp | UpdateField MisoString -- user typing in the new-todo input | EditingTodo Int Bool -- open or close the inline editor for a todo | UpdateTodo Int MisoString -- update the text of a todo being edited | Add -- submit the new-todo input | Delete Int -- remove a todo by id | DeleteComplete -- remove all completed todos | Check Int Bool -- toggle the completed flag of a single todo | CheckAll Bool -- toggle all todos at once | ChangeVisibility Filter -- switch the active filter deriving (Show, Eq)
Keeping all mutations in one flat sum type makes it straightforward to audit every state transition and to add logging or time-travel debugging.
The header contains the application title and the new-todo input field. Pressing Enter dispatches Add, which appends a new Todo to the list and clears field.
Saving — after every model update the new model is serialized to JSON and written to a fixed key:
saveModel :: Model -> IO ()saveModel m = setLocalStorage "todos-miso" m
Loading — the initial model is read from storage when the application starts. If no stored value exists, or if deserialization fails, emptyModel is used as a fallback:
getSavedModel :: IO ModelgetSavedModel = do result <- getLocalStorage "todos-miso" pure $ case result of Right m -> m Left _ -> emptyModel
Because getLocalStorage and setLocalStorage are ordinary IO actions, they integrate naturally with miso’s Effect system via io and io_.
Miso.Storage uses the browser’s localStorage API. Data is scoped to the origin and persists indefinitely until cleared by the user or the application.