Thomas Bandt

Model-View-Update (MVU) – How Does It Work?

MVU (also known as The Elm Architecture) seems to be one of those things which are a mystery to most of us. Until all of a sudden, we understand it and never wanna miss it again. It's not all that complicated.

Published on Sunday, 09 February 2020

While MVU finds itself increasingly adopted across different technology stacks, it has its origins in the community of the functional programming language Elm:

The Elm Architecture is a pattern for architecting interactive programs, like webapps and games. This architecture seems to emerge naturally in Elm. Rather than someone inventing it, early Elm programmers kept discovering the same basic patterns in their code. It was kind of spooky to see people ending up with well-architected code without planning ahead!

But enough of history, this post instead focuses on a quick introduction based on one of my current favorite programming languages, F#. I try to explain the essential fundamentals from my point of view and my current understanding of them. I am pretty sure you end up with more questions after reading, e.g., how to scale it. But if you understand what is meant by unidirectional dataflow and can see its advantages before your inner eye, then this post fulfills its purpose.

Let's Start With The Most Important Terms

  • A Program is kind of a View Model in MVVM, except it might also contain the view. So it can rather best seen as the smallest possible autonomous unit which can be executed. A program, so to say!
  • Other than in architectures like MVVM, a Model does not describe a rather arbitrary set of functionality (services, data, ...), but the state of your program. Thus many people arent' comfortable with the term and call it state instead. I stick with model here to avoid confusion.
  • A Command is what you would expect it to be. Something that can (asynchronously) perform an action, e.g., fetching data from an API, writing data back to a database, or calculate stuff. It might itself trigger sending a new message.
  • A Message is the glue that holds all the parts together. It particularly defines what the update function, which is described below, needs to perform.
  • The Init() function is the starting point of a Program. It creates the first version of your model, and it might initiate a first command, e.g., for fetching data.
  • The Update() function is the only place that directly manipulates your model. But different from other approaches, it never mutates it, but instead creates an updated copy. Along with that new version of your model, it might also return a command to initiate further actions.
  • The View() function is responsible in most MVU implementations for describing the UI.

You might have noticed that I am talking about functions, not methods. This part is important: The only thing that is allowed to cause side-effects is a command. Everything else must be free of side-effects, which not only makes a program easily testable for most of its parts but especially easy to reason about.

Show Me A Counter Example. I Love Counters!

type Model =
    { Count: int }

type Msg =
    | Increment of int
    | IncrementRandom
    | CmdIncrementRandom

let init() = { Count = 0 }, Cmd.none

let private random = Random()
let private incrementRandom () =
    (Increment(random.Next(0, 100)))

let update msg model =
    match msg with
    | Increment value -> { model with Count = model.Count + value }, Cmd.none
    | IncrementRandom -> model, Cmd.ofMsg CmdIncrementRandom
    | CmdIncrementRandom -> model, Cmd.ofMsg (incrementRandom())

let view (model: Model) dispatch =
    View.ContentPage
        (content =
            View.StackLayout
                (children =
                    [ View.Label(text = sprintf "Current Count: %d" model.Count)
                      View.Button(
                        text = "Increment", 
                        command = (fun () -> dispatch (Increment 1)))
                      View.Button(
                        text = "Increment Random", 
                        command = (fun () -> dispatch IncrementRandom)) ]))

let program = Program.mkProgram init update view

Even if you are not in love with counters, this is an example of a more or less complete MVU Program. A mobile app written in F# with the fabulous Fabulous library. It's complete because it contains all the basic concepts and ideas I listed above.

If you're not familiar with F# or any other functional programming language, it might look a bit weird, or even frightening. But don't worry, it's not that hard. Let's see what happens here.

Program

The whole thing is wrapped into an F# module, which I like to call *Program (but that's a matter of personal taste). At the end, a function of the Fabulous library takes all the necessary functions and creates a ... program!

Program.mkProgram init update view

Commands

Both the init and the update function return next to the model a command. The default here is Cmd.none, as most of those paths do not require a command to be executed. However, there is one exception:

let update msg model =
    match msg with
    ...
    | CmdIncrementRandom -> model, Cmd.ofMsg (incrementRandom())

When the message CmdIncrementRandom comes in, a new random number shall be generated and added to our current count value. As the update function itself must stay pure, we must outsource the job of generating that random number.

So we return Cmd.ofMsg, meaning that our command itself is required to trigger another message at the end of its execution. By looking at the incrementRandom function, we can see that precisely this is happening:

let private random = Random()
let private incrementRandom () =
    (Increment (random.Next(10, 100)))

The function returns a message of the type Increment which contains the randomly generated number as its "payload."

Messages

Increment is one of three different message types we allow to be processed:

type Msg =
    | Increment of int
    | IncrementRandom
    | CmdIncrementRandom 

Note the last one, CmdIncrementRandom. The naming convention Cmd* is something I am using for simplistic scenarios where I want to ensure that a particular command is being triggered through a unit test.

In this case, we could make sure that every time IncrementRandom is issued, CmdIncrementRandom follows. Testing the execution of the exact function is not possible here. Still, from my experience, this is a good enough compromise. See this post for more details and the whole thread over there for some background information.

Init

For this program, our init function is quite simple:

let init () = { Count = 0; }, Cmd.none

It creates a new instance of our model and returns it as a tuple along with Cmd.none, as there is nothing else to do here. If there would be some initial loading necessary, for example, this could be issued through a suitable command at this point.

Update

The update function is a central piece of the whole puzzle:

let update msg model =
    match msg with
    | Increment value -> { model with Count = model.Count + value }, Cmd.none
    | IncrementRandom -> model, Cmd.ofMsg CmdIncrementRandom
    | CmdIncrementRandom -> model, Cmd.ofMsg (incrementRandom())

When called, a message and a copy of our current model is being passed. Along with a command, it always returns either the same or a modified copy of the model. But never the mutated original. In a real-world application, this function gets a bit messier in my experience, but it is still a joy to work with, as it makes it clear what is going on. Debugging and unit-testing update functions mostly are pure joy.

View

The view function, in most cases, builds and returns the UI of your program. How exactly that UI is built depends on what you are building and what library you are using. E.g., mobile apps with Fabulous, or web apps with Fable or even Elm.

But one thing is essential regardless of the use case and the language. The view function itself is pure and always (*) a representation of the current state of your model. Therefore it is testable through actual unit-tests (try this with your Xamarin.Forms XAML or your iOS Storyboards :-)).

(*) Except when it isn't. There are implementations like Fabulous.StaticView and Fabulous.XamarinNative that explicitly leave out the view part of the whole MVU equation on purpose.

Unidirectional Dataflow FTW!

Reasoning about your code's logic now got a lot easier. Because every part of the program has its pre-defined role, and all parts exclusively communicate through messages in a specific order:

A graphical representation of MVU

Let's take a look at what happens when a user taps the "Increment Random" button, for instance.

First, an IncrementRandom message is dispatched, which effectively means that the update function is being called and the message is passed along:

let update msg model =
    match msg with
    ...
    | IncrementRandom -> model, Cmd.ofMsg CmdIncrementRandom
    ...

As you can see, the update function returns the (unchanged) model, and "a command of a message" with the type CmdIncrementRandom. This issues another roundtrip, causing the update function to be called a second time:

let update msg model =
    match msg with
    ...
    | CmdIncrementRandom -> model, Cmd.ofMsg (incrementRandom())

Again, the model is not changed, but a command with a message is returned. This time, however, that message is being defined by the function incrementRandom:

let private incrementRandom () =
    (Increment (random.Next(10, 100)))

What may look a bit weird for anyone not familiar with an ML language, is, in fact, quite simple. In C#, this would look like:

private Increment IncrementRandom()
{
    return new Increment(random.Next(10, 100))
}

What happens now with that Increment message? Right, it is going on a run for the third time.

let update msg model =
    match msg with
    | Increment value -> { model with Count = model.Count + value }, Cmd.none
    ...

Now it does not issue a new command, as we are done with our journey. But it does create an updated copy of the model, which causes the view function to return an updated representation of the UI. And only now that UI re-renders for the first time.

Conclusion

That's it! If you already heard about MVU before but could not grasp it, I hope you now have a better understanding. If you did not hear of it before, I hope you now like the idea. In any case, go and give it a try yourself (in whatever programming language and technology stack you prefer) and spread the word! :-)

What do you think? Drop me a line and let me know!