Is Cmd.map the right way to split an Elm SPA into modules?

Viewed 450

I'm building a single page application in Elm and was having difficulty deciding how to split my code in files.

I ended up splitting it using 1 module per page and have Main.elm convert the Html and Cmd emitted by each page using Cmd.map and Html.map.

My issue is that the documentation for both Cmd.map and Html.map says that :

This is very rarely useful in well-structured Elm code, so definitely read the section on structure in the guide before reaching for this!

I checked the only 2 large apps I'm aware of :

  1. elm-spa-example uses Cmd.map (https://github.com/rtfeldman/elm-spa-example/blob/cb32acd73c3d346d0064e7923049867d8ce67193/src/Main.elm#L279)
  2. I was not able to figure out how https://github.com/elm/elm-lang.org deals with the issue.

Also, both answers to this stackoverflow question suggest using Cmd.map without second thoughts.

Is Cmd.map the "right" way to split a single page application in modules ?

2 Answers

I think sometimes you just have to do what's right for you. I used the Cmd.map/Sub.map/Html.map approach for an application I wrote that had 3 "pages" - Initializing, Editing and Reporting.

I wanted to make each of these pages its own module as they were relatively complicated, each had a fair number of messages that are only relevant to each page, and it's easier to reason about each page independently in its own context.

The downside is that the compiler won't prevent you from receiving the wrong message for a given page, leading to a runtime error (e.g., if the application receives an Editing.Save when it is in the Reporting page, what is the correct behavior? For my specific implementation, I just log it to the console and move on - this was good enough for me (and it never happened anyway); Other options I've considered include displaying a nasty error page to indicate that something horrible has happened - a BSOD if you will; Or to simply reset/reinitialize the entire application).

An alternative is to use the effect pattern as described extensively in this discourse post.

The core of this approach is that :

The extended Effect pattern used in this application consists in definining an Effect custom type that can represent all the effects that init and update functions want to produce.

And the main benefits :

  1. All the effects are defined in a single Effect module, which acts as an internal API for the whole application that is guaranteed to list every possible effect.
  2. Effects can be inspected and tested, not like Cmd values. This allows to test all the application effects, including simulated HTTP requests.
  3. Effects can represent a modification of top level model data, like the Session 3 when logging in 3, or the current page when an URL change is wanted by a subpage update function.
  4. All the update functions keep a clean and concise Msg -> Model -> ( Model, Effect Msg ) 2 signature.
  5. Because Effect values carry the minimum information required, some parameters like the Browser.Navigation.key are needed only in the effects perform 3 function, which frees the developer from passing them to functions all over the application.
  6. A single NoOp or Ignored String 25 can be used for the whole application.
Related