Understanding the "double update" behavior of Browser.application

Hi all,

I’m working upgrading to Elm 0.19. Our main was using Navigation.programWithFlags and I’ve shifted to Browser.application. In my efforts to understand what was needed, I was reviewing experience reports like @jerith 's update experience report..

I wanted to understand a line in here:

elm-route-url does seem to still be necessary: Browser.application still has a “double-update problem”.

I have realized that I don’t fully understand the impact of switching directly to Browser.application. I’m running into some problems, and I thought I would step back to learn/understand more.

So what behavior is being referred to here? Is the update function passed to Browser.application being called twice with the same msg?

I imagine this is a wider context for the behavior. I hope to grok this, and appreciate any thoughts. I reached out to @jerith about this on Elm Slack, and he thought others may be interested in this (and would like to contribute to this topic, as well).

Thank you.

Any more background on this double-updated problem? That linked report doesn’t have any more context.

Yes, the experience report didn’t have that detail. I was reaching out to @jerith over Elm Slack to get some more details. He asked if I might start a topic here so that others might contribute.

It’s a great suggestion too, since if lots of people asked him - it might get tiring to repeat yourself.

With the topic creating, it’s a placeholder for a potential response.

The easiest way to understand the problem, I think, is by starting with the example program in the guide’s section on Browser.application. In that example, the control flow for clicking on a link looks like this:

<a href="/path"/>
    \
     \- click on it -->-- Elm runtime >-\
                                         onUrlRequest function
                                           |
                                           v
                                         LinkClicked message
                                           |
                                           v
                                         pushUrl cmd
                      /-< Elm runtime <-/
  onUrlChange function
    |
    v
  UrlChanged message
    |
    v
  update function enacts model update for "/path" URL

There is a double-update in a sense here, because as you can see we have two messages (LinkClicked and UrlChanged) flowing through two invocations of our update function. However, they’re well-ordered and serve two different purposes, pretty clearly explained in the guide.

In 0.18 and earlier, things were not nearly this nice. The documentation for elm-route-url explains this pretty well, but in short you had a lot of bookkeeping to do on your own. I sank quite a lot of time into trying to make that work before discovering elm-route-url, and it was a pain.

In the above diagram, it’s important to note that, if you want any data to ride along with the user’s click on that /path link, you must encode that data in the URL itself. And then you have parse it back out when it comes into (the second invocation of) your update function.

That parsing work does have to be done if you want to support “deep linking” into your SPA. And the encoding work does also have to be done if you want to generate those deep links. But it feels like a big sacrifice to give up the strongly-typed messages that you see in most Elm architecture examples. :-/

So, given all of that, my app in 0.18 and before works off of divs that produce messages onClick. However, doing it that way, as in this modification to the guide’s example code, yields this control flow:

<div onClick="Goto"/>
    \
     \- click on it -->-- Elm runtime >-\
                                         Goto message
                                           |
                                           v
                                         pushUrl cmd
                /----< Elm runtime <----/  |
  onUrlChange fn                           |
    |                                      |
    v                                      v
  UrlChanged message --> update fn enacts model update for Goto message

This is the double update problem: there are two arrows going into the update function that both want to enact the model update. If you run the code, you’ll see that clicking on the <a/>s increments the counter by one, but clicking the <div/> increments it by two. (You can’t just ignore the UrlChanged message because that’s the only one that happens on back/forward navigation.)

So, if you take the approach of driving your app with div onClick rather than a href, then elm-route-url is still necessary in order to take care of the bookkeeping and prevent the double updates. Unfortunately we’re still working on updating it for 0.19 – help is appreciated! :slight_smile:

(This example program just needs some very trivial bookkeeping to make it work, but in general it’s more complex.)

I think it would be a nice addition if pushUrl took a type ChangeNotification = InvokeUrlChange | SkipUrlChange argument; that would really alleviate the problem.

4 Likes

Thanks Matt. I phrased the topic as the “double update” behavior because I really didn’t have the before to see if it was a problem. I’m mulling what you’ve provided and I am quite grateful for this detail and thoughtfulness.

I should add that in general I think it’s preferable to use a hrefs, you get a lot of things for free with them (hover states, open-in-new-tab, etc. etc.) that you have to build yourself or live without if you use div onClicks. So maybe what I really want is a way for an a href to say “i don’t need to handle this twice in update, give me the short-circuit pure-elm strongly-typed alternative”. (There’s a lot of hand-waving there, hopefully you understand my high-level point though.)

I think I understand it at a high level. In the upgrade effort I’m working on, I was confused about how to handle a href and was going down the onClick path.

The application I’m work on also has a complexity that I don’t want to bother too many with … but the Elm app is hosted within another application (an ASP.NET MVC app) so you’re point about "External might not have a different domain" lands for me.

In essence, I moved the app from Navigation.programWithFlags to Browser.application without a proper, strong understanding of that impact. Thank you for politely pointing back to the docs for Browser.application.

Hm … if you’re dealing with an app that doesn’t control the whole page, you should also consider the issues discussed at the docs for Browser.Navigation.Key and the “annoying bugs” link therein. You probably don’t want to / can’t use Browser.application in your case for those reasons anyway! :slight_smile:

1 Like

The application controls the “whole” page for a sub-select of possible pages.

I have been considering that I need to shift to Browser.element, even if that entire pageview is rendering by Elm.

And, apologies for diverting attention from the central topic.

So as I understand you want to pass additional data when navigating, without having to serialise and deserialise from the URL. What copying links and the back button. I think that having to serialise in the URL is a good thing when working with the web platform.

Also:

Goto url ->
            ( { model | url = url, n = model.n + 1 }, Nav.pushUrl model.key <| Url.toString url)

In here I don’t think you should set things in the model, it should probably be:

Goto url ->
            ( model, Nav.pushUrl model.key <| Url.toString url)

But I don’t see how setting the url twice could be a problem.

1 Like

Setting the URL twice is a problem because you end up with duplicate history entries. So the user has to push back twice when they expect to only have to push it once.

Sorry, I meant setting the URL twice in the model as shown in https://raw.githubusercontent.com/jerith666/elbum/6da530843348368081d4dcfba159a32ae5007565/src/client/AppExample.elm. This doesn’t create duplicate history.

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