Introduction
WebSharper.Mvu provides an Elm-inspired MVU (Model-View-Update) architecture for WebSharper client-side applications.
It is based on WebSharper.UI for its reactivity and HTML rendering.
The MVU architecture
Model-View-Update is an application architecture that aims to make the behavior and state of GUIs clear and predictable.
The state of the application is stored as a single Model, which is an immutable value (generally a record).
This is rendered by a View, which defines how the model is transformed into DOM elements. (Although in WebSharper.Mvu we tend to use the term Render instead, to avoid confusion with the WebSharper.UI View
type.)
Finally, all changes to the model are applied by a pure Update function (also called reducer by some frameworks like Redux), which takes messages sent by the view and applies changes accordingly.
Differences with other MVU libraries
The main point that differentiates WebSharper.Mvu from other MVU libraries is the way the render function works.
In most MVU libraries, the view function directly takes a Model value as argument. It is called every time the model changes, and returns a new representation of the rendered document every time. This new representation is then applied to the DOM by a diffing DOM library such as React.
In contrast, in WebSharper.Mvu, the render function takes a WebSharper.UI View<Model>
as argument. It is called only once, and it is this View
that changes every time the model is updated. This helps make more explicit which parts of the rendered document are static and which parts are reactive.
See the WebSharper.UI reactive programming for more information on how to use View
.
How to create an MVU application
Follow these steps to get started with a simple component/application:
- Define a
Model
type that represents the state of your component/application. Usually, this is a record type with fields for each piece of state you want to keep track of. - Define a
Message
(sometimes also called Action) type that represents the actions that can be performed on the model. Usually, this is a discriminated union type with one case for each action. - Write an
Update
function with the signature(msg: Message) -> (model: Model) -> option<Model>
. This function takes a message and the current model, and returns a new model if any change is made. ReturnNone
if no change is made, which leads to less unnecessary re-rendering. - Write a
Render
function with the signature(dispatch: Message -> unit) -> (state: View<Model>) -> Doc
. You can create the UI based on theView
and use thedispatch
function to send messages through the updater. The return type is not fixed, you can also bind to the page directly and returnunit
. - Tie it together with
App.CreateSimple initModel Update Render |> App.Run
, whereinitModel
is the initial state of your component/application.
We will see more advanced features later.
Composability
Bigger WebSharper.Mvu applications are built from smaller components, each with its own model, update function, and render function. This allows you to compose complex applications from simpler parts.
For example, you can have a Counter
component with its own model and update function, and then use it in a larger application.
Here is an example of an application that implements a simple counter:
You can iterate on this example to create a list of counters that operate independently, each with its own state:
Advanced Update functions
App.Create
is more powerful than App.CreateSimple
.
It allows you to define a more complex update function that can decide on how to update the model with more available options by returning an Action<'Message, 'Model>
.
This type is a discriminated union with cases:
DoNothing
: Does nothing, it is useful to avoid unnecessary re-rendering.SetModel of 'Model
: Sets the model to a new value, replacing the current model.UpdateModel of ('Model -> 'Model)
: Updates the model by applying a function to it, this is useful when you are chaining multiple updates withCombinedAction
.Command of (Dispatch<'Message> -> unit)
: Executes a command that can send messages to the update function. This enables recursive updates.CommandAsync of (Dispatch<'Message> -> Async<unit>)
: Executes an asynchronous command that can send messages to the update function. This is useful for handling asynchronous operations such as HTTP requests.CombinedAction of list<Action<'Message, 'Model>>
: Combines multiple actions into one. Note that aCommandAsync
will not be awaited, only started.
Features of WebSharper.Mvu
WebSharper.Mvu provides a number of features on top of this architecture.
Automatic local storage
WebSharper.Mvu can automatically save the model to the local storage on every change. This allows you to keep the same application state across page refreshes, which is very useful for debugging.
This is done by adding a single line to your app declaration:
If you want to use both local storage and Redux DevTools integration, you must add App.WithLocalStorage
before App.WithReduxDevTools
.
Paging
The Page
type makes it easy to write "multi-page SPAs": applications that are entirely client-side but still logically divided into different pages. It handles parameterized pages and allows using CSS transitions between pages. Pages can specify their DOM behavior, such as keeping elements around to allow for smoother transitions.
Here is a small application that demonstrates this.
Create a paged application by using App.CreatePaged
or App.CreateSimplePaged
. They are constrained in only that the Render
function must return a Page
value.
The pages are created as singleton values. This is important so that the MVU framework can keep track of renderings and reuse them when the page is revisited. Page identity is will also determine when to apply transition animations if you add css for it.
The example uses css classes home-page
and entry-page
and to create the transition effect when switching between the two pages. Switch to the HTML tab to see the CSS code.
Create pages by one of the following methods:
Page.Single
: A single instance of the page is created lazily, the first time it's needed then reused.Page.Create
: An instance of the page is created for each different endpoint value, and reused when the endpoint is the same.Page.Reactive
: Takes a function that reduces an endpoint to a key value, a single instance of the page is created for each key value, and reused when the key is the same.
Optional parameters:
attrs
: extra attributes to apply to the page wrappingdiv
.keepInDom
: iftrue
, the page will not be removed from the DOM when it is not active (only hidden), allowing for smoother transitions. This is useful for pages that have expensive initializations or animations.usesTransition
: iftrue
, uses atransitionEnd
event handler instead of removing the page instantly when navigating away from it. This allows setting up CSS transitions for the page, but it's not done automatically by thePage
helper.
Routing
The page's URL can be easily bound to the application model. The URL scheme is declared using a WebSharper router, and the parsed endpoint value is stored as a field in the model.
Routing and paging work nicely together, but neither requires the other. Routing is implemented by adding a single line to your app declaration:
This will automatically update the URL when the model changes, and parse the URL to update the model when the page is loaded.
App.WithRouting
uses the getRoute
function to extract the endpoint from the model, and automatically infers the inverse to update the model from the route.
If you want to specify both the getRoute
and setRoute
functions, use App.WithCustomRouting
.
Setup actions
To start running some application logic automatically on startup, you can use WithInitMessage
or WithInitAction
. This will execute the given message or action when the application starts, before the first render.
Logging and debugging
Use App.WithLog
to set up a logging function that will be called with every message sent to the update function. This is useful for debugging and understanding the flow of messages in your application.
Use App.WithReduxDevTools
to integrate with the Redux DevTools for debugging. This allows you to inspect the state of your application, replay actions, and time travel through the history of your application.