Have you found it painful to split up your application into modules/pages and to setup the messaging structure correctly? It can be tricky to figure out how to handle Cmds and Msgs across files. And it’s still not easy to find examples of how to architect larger apps (and even if you find a large code base, it will be tricky to trace what exactly is going on).
But it doesn’t have to be mysterious! What you need is the principles to guide your choices about organising code and splitting it up into files. You can then follow them, and keep your code maintainable as it grows.
It turns out that Elm has a couple of very useful principles baked into the design of the language:
view
function and only one update
function that the Elm runtime interacts with (and consequently, one set of messages).It might seem that I’m stating the obvious, however let’s expand on these a bit to see where it takes us.
First of all, I’d like to suggest not enforcing an arbitrary size limit on files. See how far you can get without breaking it up into multiple files - it may just be that you acquired a habit of working with small files from other languages where it’s encouraged.
Nevertheless, at some point functions will become too long and getting around the file will become too onerous. What can you do then? There are simple techniques provided by Elm to introduce structure.
View function is too long? Break it up into multiple functions:
view : Model -> Html.Html Msg
view model =
viewport styles <|
column Base
[ center, width (percent 100) ]
[ navBar -- helper function
, menuPanel model.appState -- another helper function
, content -- and another
]
Msg
type has too many messages? Extract subtypes:
type Msg
| Auth AuthMsg
| Display DisplayMsg
| Register RegistrationMsg
...
type AuthMsg
= ChangePassword String
| ChangeUserName String
| RequestLogout
| StartLogin
type DisplayMsg
= MouseEnteredPlanNode Plan
| MouseLeftPlanNode Plan
...
The update
function is too complicated? Extract helper functions for each case
clause.
Too many data types and functions in one place? Extract some of them into a module.
Too many modules? Organise them into subdirectories.
This is made easy by the language, and it can take you quite far in terms of growing the code base - without the concept of a framework.
A lot of developers coming to Elm have questions about breaking up the application into some sort of “components” with their own state, there are discussions of stateful widgets and so on.
However, the important thing to understand here is that no matter what you do, you are still ultimately passing a single update
function and a single view
function to Html.program
or an equivalent. All the messages the runtime generates will still go into that update
function and nowhere else.
Even if a module contains an update
function and a Msg
type, there is no mechanism for it to receive messages. The module code will have to be wired into the “main” update
function and the “main” Msg
type. Similarly, the “main” view
function simply calls all the other functions that render UI widgets or whole pages. So the interaction between the Elm runtime and your program remains very straightforward conceptually. This wiring is what people refer to when they talk about model-update-view triplets.
Essentially, you need to call a function that handles a subset of messages from the main update
function, and then use its return value to make changes to some fields of the model:
-- One of the `case` clauses in the main `update` function:
( Register regMsg, RegistrationPage m ) ->
let
-- Return some data based on the subset of messages related to registration
( regModel, regCmd ) =
Registration.update regMsg model.appState m
-- Construct the new model value based on the data returned
newModel = { model | currPage = RegistrationPage regModel }
in
( newModel, Cmd.map Register regCmd )
There is some wrapping and unwrapping of values involved, but this is really quite straightforward. It can get more involved depending on the modifications to the model required as a result of these messages, but it wouldn’t require any new concepts.
On the flip side, I’ve also seen a different type of complaints about how the Elm architecture forces parent components to manage the state and structure of all children components, or how there is no good alternative to using model-update-view triplets for code organisation.
I think what those people are missing is that similar wiring exists in JavaScript as well (for example), it’s just hidden in the framework du jour. Elm doesn’t include a framework, so indeed, you have to do it yourself! However, you could write an Enterprise Elm Framework which would put most of the wiring under sleek covers. But of course it would also impose a particular structure on your code: your widgets and pages would have to conform to certain patterns and interact in the ways prescribed by your framework.
As far as being forced into a particular approach (model-update-view triplets), that’s true, but this is also where the benefits of Elm come from, and I think understanding that will help you view the constraint in a different light. In a sense, I think this complaint is also part of a debate about boilerplate vs. the number of concepts and abstractions required for the most concise code, where Elm definitely leans towards lowering the number of concepts and abstractions. For some people, it’s an unacceptable tradeoff, but I encourage you to consider whether this in fact has material effect on your productivity and quality of the code.
I’ve seen that this subject trips a lot of people up, so I’ve expanded the section on code organisation in my book, Practical Elm, into a full chapter that walks through extracting a stateless UI widget, some non-UI code, as well as splitting up the code into a module per page. (If you’ve already bought the book, you will receive an updated version with this chapter.)
There are a couple of useful talks on the subject of organising code:
Additionally, Richard Feldman has created an example single-page application and wrote a post to describe it:
This code base shows an example of the module-per-page approach and managing the dreaded model-update-view triplets.