Chapter 4. The Elm Architecture

Elm programs have a standard architecture which consists of a data model, a view function that renders that model into HTML, and an update function that handles all updates to the model. You might find this familiar, as it’s a variation of the Model-View-Controller pattern.

The first step in writing an Elm app is to register these components with the Elm runtime. A basic Elm application looks like this:

main =
    Html.beginnerProgram
          { view = view
          , model = 0
          , update = update
          }

type Msg
    = Increment
    | Decrement

update : Msg -> Model -> Model
update msg model =
    case msg of
        Increment ->
            model + 1

        Decrement ->
            model - 1

view : Model -> Html Msg
view model = div [ onClick Increment ] [ text (toString model) ]

Notice that it doesn’t matter in which order everything is declared in our code (in our main definition, we reference our view function and our update function even though they are defined later in the file). This is because our file isn’t executed from top to bottom, but is instead a collection of types and functions. Execution is handled separately by the Elm runtime using the functions that we provide it.

All the data used in an Elm application is described in the data model. Commonly this is captured as an Elm record, but any type can be used as the model. In this example, our model is just a number. We didn’t need to tell the compiler this because it inferred that fact when we used addition and specified the initial value as 0.

Our update function takes a Msg and our Model and results in an updated Model. Msg is a normal union type like we discussed in Chapter 2. These Msgs are handed to the update function when the specified event fires. This means our update function generally takes the form of a comprehensive case expression. In human terms, this means the update function serves as a table of contents for everything that can be done to our model. The update function is also the only place where changes to the model can be made. This drastically simplifies navigating the code and tracking down where something happens.

Elm’s view function makes use of a virtual DOM. Similar to in React, in Elm you describe your view as functions, which the Elm runtime will render as HTML. When your model changes, the virtual DOM performs a diff operation to see what needs to be updated, and then makes the necessary changes to the DOM as efficiently as possible.

Upon closer inspection, we can see that our view can also send Msgs to our update function. In our view we have onClick Increment, which should feel intuitive. When we put this in our code, the Elm runtime takes care of calling our update function with the value Increment when a click event occurs.

Standardizing the language on a strong architecture means that beginners and experts alike have a solid starting point for well-organized code. It also simplifies navigating other developers’ Elm code, because you know to look for a model, a view, and an update function.

Interop with JavaScript

The main method that JavaScript can use to communicate with Elm is via ports. Data coming in from JavaScript land first needs to be translated into Elm types. For most common types this can be done automatically. For something more nuanced like Elm’s union types, where we don’t have a direct analog in JSON, you’ll have to write a small bit of code called a JSON decoder, which creates the type you need from the data that’s provided. There are also tools that can automatically generate Elm decoder code directly from JSON . They can’t generate code for every situation, but they can get you most of the way there and point you to what still needs attention.

This process is fairly straightforward (and beyond of the scope of this report) and is how most communication with Elm works. When outside data comes in, it’s checked at the gate. If the data isn’t well formed, then Elm doesn’t throw a runtime error (that would be silly), but instead captures an error message describing what went wrong and provides it to you in a manner where you can have your application gracefully deal with it.

Adopting Elm Incrementally

There’s no need to adopt Elm for the entirety of your project in one go. You can adopt Elm incrementally by giving it control of a single HTML node of your app. That way you know that this one piece of HTML is completely managed by Elm, and the rest of your page can be run by other technologies as you see fit. This is obviously useful when dealing with a large codebase that’s already in production.

Here’s some basic HTML that shows how to embed Elm code that has been compiled to my-elm.js and attach it to an HTML node:

<body>
  <div id="elm"></div>
</body>
<script src="my-elm.js"></script>
<script>
    var node = document.getElementById('elm');
    var app = Elm.Main.embed(node);
</script>

Some people have even done work that shows how to write React components in Elm, so more sophisticated incremental integration of Elm is possible.

Elm Versus React

React is a JavaScript library made for the view part of the frontend, which means taking your data model and rendering it as HTML.

Both React and Elm utilize the idea of a virtual DOM, which decouples declaring what HTML you want and actually rendering the changes as HTML. This is done so that all changes to the DOM can be done in one batch and rendered efficiently by the browser. This decoupling is great for another reason: it allows for a much more declarative style of programming. Instead of creating a tangle of functions that can each modify the DOM independently, we can describe what the HTML should look like for a given model and let a function calculate what changes need to be made.

Many of the differences between React and Elm can be traced back to the languages themselves. In Elm, we still have the guarantee of no runtime exceptions in practice. We’re also able to catch virtually all trivial—and a large number of meaningful—errors at compile time without needing to run our program in a certain way with a certain state to see if it errors. React does have some type-checking abilities, but this is all done at runtime and requires the code with the bug to be executed before React logs an error. It’s also not nearly as comprehensive as Elm’s checking.

React is designed to support using immutable data to store state, though it can’t make you use it in your entire project. Because of this, there are a number of subtle bugs that can occur in your React code as a result of hidden mutable state. An example of this (as given by the React documentation) is that a component may not render if you mutate certain props. This is behavior that would be hard to mimic in Elm. If there was an issue like this in Elm code, it’d likely show up as a compiler error. The React documentation recommends focusing on maintaining immutable state to avoid these issues. This is in contrast to Elm having data immutability built in, which means you can enjoy the performance benefits without having to worry about subtle pitfalls of switching between mutable and immutable data.

React incorporates some of the same ideas that power Elm, but can only fully implement a portion of them due to the limitations of JavaScript. It’s forced to implement the rest as recommendations and incremental (meaning not language-wide) tools. You can get some of the reliability of Elm with React, but only if you know all the subtle traps to avoid and have the time to ensure that you’ve avoided them.

Many best practices in Elm are simply built into the language. In React (and most projects written in JavaScript), best practices and warning signs are communicated through the documentation. The question is, would you prefer be continuously scouring the documentation for these hidden surprises, warnings, and performance tips, or do you want the lion’s share of this work to be taken care of by the compiler?

Elm Versus Vue.js

There’s a trend toward libraries being more stripped down and closer to the technologies that they’re trying to use (HTML, CSS, and Vanilla JS). This is the general approach of Vue.js, which makes the framework feel incredibly familiar. The idea is that Vue.js allows you to write simple JavaScript with the hope that when something does break, there won’t be mountains of abstract indirection to throw you off course.

Vue.js doesn’t change anything fundamental about JavaScript. It can’t provide you with the high-level benefits that are built into Elm. If you’re worried about code breaking and being hard to debug, maybe the answer isn’t to write simpler JavaScript but to use a language that has effectively eliminated runtime errors in the first place and provides amazing, specific compile-time errors that tell you exactly why something might break.

It’s tempting to think that Vue.js will be easier to learn than Elm. While Elm may seem foreign to established frontend developers, much of the learning curve comes from habits that are ingrained from having done things differently for many years and having to reevaluate our coding intuitions. Let’s be real—given a week, I’d be surprised if a competent web developer couldn’t become productive in Vue.js or Elm. The learning curve for each is generally low, and both have excellent documentation.

Once you get past the initial learning curve, however, Vue.js and Elm aren’t equal. In my opinion, beginners who code in Elm end up with maintainable, performant, runtime error–free code from the start because these things are built into the language. The same can’t be said for Vue.js code.

Now that we’ve looked at the standard Elm architecture, let’s take a look at the tools that are available to us when we program in Elm.

Get Why Elm? now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.