A wrestling match: incoming messages versus altering model

Hello folks!

I’ve been using Elmish (An F# implementation of Elm) to develop a few rather large applications over the last three years. Each one uses data fetched and cached from a server, and contains multiple pages (with routing between them) and about a dozen re-usable UI components (a table, a collapsible tree with drag-drop, a grid view with a data display, etc).

Things are going pretty well but I’ve come up against a design issue that is a bit philosophical, and I wanted to get some input from others. The issue is best described with a basic example.

Say you have web page with some stuff on it, including a graph on one side. The graph is implemented as a reusable component – a module with its own model, view, and update.

The graph depends on a set of data which is fetched from the server. That data takes a while to fetch. It changes on a regular basis, and the graph has to redraw whenever it changes. Several other parts of the page need to redraw too.

The question is this: What’s the clearest way to get that changed data into the graphing component?

Approach 1:
Act as though the data doesn’t “belong” to the component. Instead of putting it in the model, pass it into the view function, taking it from some enclosing model. The component’s model never sees it. How stale it is, is someone else’s problem. (The parent’s.)

Issues:

  • The component’s model no longer contains all the state the component needs to work, gathered in one place. This is potentially confusing.
  • The user of the component now needs to deal with a custom view function.
  • If we want to write any logic in the update function that uses this data, we need to pass it in as part of a message. This may introduce some skew between the data we rendered in the view, and the data we’re reacting to in “update”.

Approach 2:
Have the enclosing component alter the graph’s model directly, swapping in the new data, somewhere in the enclosing component’s update function.

Issues:

  • The graph component’s update function used to be THE ONLY code that manipulated its own model. Now that’s no longer true. A developer reading this code can no longer understand how the module works just by looking at the model, update, view.
  • Any developer working with this component now needs to place additional code in the enclosing component. How will they know when and where? We’ll just have to rely on some README document or code comment.

Approach 3:
Write a function within the graphing module, publicly exposed, with the name “updateSourceData”, which accepts a model and the new data, and emits the model with the new data embedded. Write commentary on the function, explaining to the developer when and where they need to call it. Now at least it’s clear how to get new data into the module.

Issues:

  • Like approach 2, this introduces manipulation of the model outside the update function. But, at least there is a clear way to do it, controlled more-or-less by the module.
  • The flow of data is confusing: The graphing module’s model is contained in a parent module. The parent is already passing that model down into the graphing module’s “update” function to handle wrapped messages, and possibly receive “external” messages back. Now the parent is passing that model to some other function inside the graphing module as well, in a separate operation. (Do we do that before we call “update”, or after? Better read the docs.)

Approach 4:
Along with the usual set of messages that are handled internally by the graphing component to do its business, declare an “IncomingMessage” type. Above that, declare a handful of sub-types of “IncomingMessage”, including one called “UpdateSourceData x” where x is the data type. Add commentary explaining when to send it. Handle these messages in the update function like all the others. In the parent module, whenever you want to give new data to the graph, generate a wrapped message for it, of type “IncomingMessage (UpdateSourceData x)”. Users of this component no longer need to directly manipulate the model, for any reason. All the code that alters the model is contained inside the graphing module’s update function, in fitting with the philosophy of Elm.

Issues:

  • Hmm, messages in and messages out. We appear to have re-created Objective C.

So, what approach do you folks like? Or is there one I haven’t described here?

Personally, I’ve come around to favoring approach 4, because it results in the smallest amount of code outside the standard Elm template. We send down wrapped messages, right alongside the ones wrapped for the component itself, and we only need to call “update” and “view” like usual. The model carries everything the component needs to render.

An additional note about rendering performance:

Approach 1 and 2 have problems when updating the view is expensive. In approach 1, the component has no idea whether the data has actually changed relative to the last render. In approach 2, the component might possibly be able to compare the new data with an older copy stored in the model, but it would have to make that comparison every time, and just the comparison itself might be expensive. (Unless you want to rely on some sort of pointer comparison witchcraft.) With approach 3 and 4, it’s clear to the module when the data is expected to be changed, and it can do something like set an internal flag to force a refresh, if that’s necessary.

This may not matter, depending on what framework you’re rendering with and the complexity of what you’re rendering, but let’s say in the case of this graph example, it does matter: If we were to try and let something like React automatically handle refreshes, we’d still need to process the data (perhaps 100,000 records) into HTML or SVG and hand it to React with every call to “view” – a huge waste of CPU time. (In my code, this problem is solved by creating a small custom React component inside the graph module, designed to update only when a counter in the model changes relative to its own counter, and the counter is incremented when handling the “UpdateSourceData” message.)

1 Like

One important point is that if the widget is complex enough, having its own state and update is perfectly fine. The argument against components is really against “components first” where everything is split needlessly into modules.

But if you have a section of the code that starts to be in that 500-1000 lines of code size, and has complex interaction with some server, it makes sense to extract it into a component with its own State, update, Msg, view.

Now, you have indeed two main options:

  1. the parent handles the data and all the performance optimizations (debouncing, caching, lazy) OR
  2. the component itself handles all this.

I too gravitate towards the second option.

For messages flowing inside the component, the issue for me is simple: I always provide an exposed function with signature fooName : SomePayload -> Model -> Model or fooName : SomePayload -> Model -> (Model, Cmd Msg) if needed

If the component needs to communicate upwards, things complicate a little bit. You can use OutMsg or you can use an update config record with lift : Msg -> msg and various outer messages that need to be generated.

There is a variation on the second in which the update receives a config object that contains command generators for the calls to update the data. This allows external configuration and external handling of communication errors (if need be).

for example:

update : { updateData : DataPayload -> Cmd msg, lift :  Msg -> msg }  -> Msg -> Model -> (Model, Cmd msg)
update cfg msg model = 
    case msg of 
        RefreshData ->
             (model, cfg.updateData GotData) 
        ...

updateData : DataPaylod -> Model -> (Model, Cmd Msg) 
updateData data =
    {model | data = data, alert = Ui.alert "Got fresh data"}, Helpers.afterSeconds 5 RemoveAlert) 

and in parent you would have:


update msg model = 
    case msg of 
        ComponentMsg cMsg -> 
            let 
                updater = 
                    Api.getGraphData user.token dataSourceUrl (Result.Extra.unpack HandleError (ComponentMsg << Component.updateData)) 
                (cModel, cmd) = Component.update {updateData = updater, lift = ComponentMsg} cMsg model.componentModel
            in 
            ( { model | componentModel = cModel } , cmd) 

Of course, if you don’t need to handle the errors in the parent, you can simplify things and just send some url in the config and let the component handle everything.

What is the best approach depends, to a certain degree, on the architecture of the entire app.

2 Likes

If the data is used in multiple places, I would strongly avoid duplicating across different “components”. A single source of truth makes your life much easier! As such my preference is for variations on your approach 1.

It’s worth noting that Elm has no “components”. We just have helper functions that can calculate values given parts of the global state tree. This informs how we might think about the issues with approach 1 you mentioned:

Custom view

view is not a magic framework function name. You can still call the function view even if it takes multiple arguments. You could also name it something else. Both of these styles are valid and not unusual to see in an Elm app:

module LineGraph exposing (view)

view : ViewingOptions -> List Points -> Html Msg
view options data =
    ...
module Graph exposing (line)

line : ViewingOptions -> List Points -> Html Msg
line options data =
    ...

Data in the update

Just like view, update is not a magic framework function and you can give it more than one argument if you want so something like this is totally valid and even expected:

update : Msg -> List Point -> ViewingOptions -> (ViewingOptions, Cmd Msg)

This is the same data being used to render the view so there is no skew happening.

Everything together in one place

In general I think this is fine. It’s not unusual for “components” to need to be passed some extra external data. For a graphing “component” in particular, I wouldn’t expect it to own the actual points.

That said, if you really want to there’s nothing preventing you from combining the separately stored points and options together before invoking your sub-view and sub-update. The important thing is that this compound structure is not stored on the model. Instead, it is derived at runtime. Otherwise you are back to where you started with invalid states.

module LineGraph exposing (view)

type alias Model =
  { options : ViewingOptions
  , data : List Point
  }

view : Model -> Html Msg
view { data, options } =
  -- ...

then in your main view

-- Main.elm
view : Model -> Html Msg
view model =
  main_ []
    [ LineGraph.view { options = model.lineGraphOptions, data = model.points }
    , Stats.view model.points
    ]
1 Like

My first thought is that your Approaches 1 to 4 don’t include an option to have the graphing module also do the fetching from the server. If you put that into the graphing module too, all of its functionality is encapsulated in a single module, and the external interface becomes simpler.

module Graphing exposing (Model, Msg, init, update, view)

type alias Model = 
    { ... -- The graph data set.
    }

type Msg
    = FetchedData ...
    | UserClickedOnGraph ...
    | AndSoOn

init : ( Model, Cmd Msg )
init =
    ( { ... }
      -- Some sensible starting state for the graph (maybe invisible until first data).
    , Http.get
        { url = "https://example.com/graphdata"
        , expect = Http.expectJson FetchedData graphDataDecoder
        }
    )

graphDataDecoder = 
    -- Decode your graph data from JSON
    ...

update : Model -> Msg -> (Model, Cmd Msg)
update model msg = 
    case msg of
        FetchedData data ->
            -- Incorporate the new data into the model.
            ...

view : Model -> Html Msg
view model = 
    -- Draw the graph from the model
    ...

Maybe the init function also needs to take some Config that might descibe options relating to how the graph is displayed, or the URL its data is fetched from and so on.

This way the user of the graph module needs to know the least amount about how it works internally.

Also note - The graph Model is kept inside the parent model, and the Cmds are mapped into the parent message type using Cmd.map.

That makes excellent sense, yes! In the codebase I’m working with, the UI widgets didn’t start out as components, but they all contain so much drawing and event-handling code that as soon as we wanted to use a widget in two places we definitely needed to make it into a component.

The OutMsg pattern is exactly what we’re using already. :slight_smile:

Aha, I hadn’t thought of this approach. It’s distinct enough from the others that I hereby call it Approach 5 :slight_smile:

I can still do caching and cache invalidation with it, too, by introducing an intermediate layer between updateData and Api.getGraphData. Interesting.

1 Like

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.