# Solving Elm Router "Double Update" Problem

**URL:** <https://discourse.elm-lang.org/t/solving-elm-router-double-update-problem/10436>\
**Category:** Show and Tell\
**Created:** [September 17, 2025, 5:20am UTC](https://discourse.elm-lang.org/t/solving-elm-router-double-update-problem/10436 "2025-09-17T05:20:12Z")\
**Posts on this page:** 6\
**Page:** 1

<div class="post-metadata">

**Author:** ![rinn7e](https://avatars.discourse-cdn.com/v4/letter/r/6a8cbe/32.png) [@rinn7e](https://discourse.elm-lang.org/u/rinn7e)\
**Post date:** [September 17, 2025, 5:20am UTC](https://discourse.elm-lang.org/t/solving-elm-router-double-update-problem/10436/1 "2025-09-17T05:20:13Z")

</div>

I found some older discussions on this issue, but they did not really provide a clear answer:

- [Understanding the “double update” behavior of Browser.application](https://discourse.elm-lang.org/t/understanding-the-double-update-behavior-of-browser-application/2963)
- [Routing when you are already there](https://discourse.elm-lang.org/t/routing-when-you-already-there/7977)

It turns out I discovered a simple solution, so I am writing it down in case I forget, or in case someone else finds it useful.

Imagine we have an expensive `parseAppRoute` function that performs many effects. We do not want it to run twice: once for `Navigate` and again for `UrlChanged`. (I am ignoring `LinkClicked` in this explanation, since in my app I only use `Navigate`, but the principle is the same.)

The idea is to keep track of a boolean flag called `isInternal` that indicates whether the URL change originated from inside the app or from an external action such as the browser’s back/forward buttons. By default this flag is `False`, because back/forward navigation can happen at any time.

Whenever I change the route from inside the app, I set `isInternal` to `True`. Then, when the follow-up `UrlChanged` message arrives, I check the flag:

- If it is `True`, I ignore the message and reset the flag to `False`.
- If it is `False`, I know the change came from the browser (back/forward), so I call `parseAppRoute`.

This way we avoid calling handling the route change twice.

On initial page load, the route is handled in `init`, so there is no issue there either.

Here is an example implementation:

```elm
parseAppRoute : String -> (Route, Cmd Msg) 
parseAppRoute url =
   let
      newRoute = urlStringToRoute url
   in
      (newRoute, getCmdFrom newRoute)

cmdFromRoute : Route -> Cmd Msg
cmdFromRoute route =
    -- perform expensive side effects

init : Flags -> Url -> Nav.Key -> ( Model, Cmd Msg )
init _ url key =
    let
        (initRoute, initCmd) = parseAppRoute url
    in
    ( { route = initRoute
      , isInternal = False
      , key = key
      }
    , initCmd
    )

-- UPDATE

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        UrlChanged url ->
            if model.isInternal then
                -- Ignore the UrlChanged that we triggered ourselves;
                -- then reset the flag.
                ( { model | isInternal = False }, Cmd.none )

            else
                -- Triggered by browser back/forward navigation
                let
                    (newRoute, newCmd) = parseAppRoute url
                in
                ( { model | route = newRoute }, newCmd )

        Navigate route ->
            let
                href = toUrlString route
                newRouteCmd = cmdFromRoute route
            in
            ( { model
                | isInternal = True -- Mark this as an internal change
                , route = route
              }
            , Cmd.batch [Nav.pushUrl model.key href, newRouteCmd]
            )

        LinkClicked req ->
            case req of
                Browser.Internal url ->
                    -- Treat internal clicks like Navigate
                    let
                        (newRoute, newCmd) =
                            parseAppRoute url
                    in
                    ( { model | isInternal = True, route = newRoute }
                    , Cmd.batch
                        [ Nav.pushUrl model.key (Url.toString url)
                        , newCmd
                        ]
                    )

                Browser.External href ->
                    ( model, Nav.load href )

        None ->
            ( model, Cmd.none )

```

I hope to hear from others if they reach the same conclusion. Feel free to ask me anything as well.

---

<div class="post-metadata">

**Author:** ![vitalije](https://yyz1.discourse-cdn.com/flex035/user_avatar/discourse.elm-lang.org/vitalije/32/4750_2.png) [@vitalije](https://discourse.elm-lang.org/u/vitalije)\
**Post date:** [September 17, 2025, 9:02am UTC](https://discourse.elm-lang.org/t/solving-elm-router-double-update-problem/10436/2 "2025-09-17T09:02:00Z")

</div>

It seems to me (maybe I am wrong), that you would achieve the same effect if you just parse url and generate effects in the branch for `UrlChanged` message. I expect that this message would be delivered only once as a follow up to `LinkClicked` or `Navigate`. In other words just after the `Nav.pushUrl` command is executed.

The way you wrote it, you have parsing urls and calculating effects on three different branches and therefore this parsing and calculation might get run more than once, so you must add a flag `isInternal`. If you just let the messages `LinkClicked` and `Navigate` return unchanged model with the effect `Nav.pushUrl`, than in the next update call `UrlChanged` will be delivered and only then you actually change the route inside the model and perform necessary effects.

HTH

---

<div class="post-metadata">

**Author:** ![jerith](https://yyz1.discourse-cdn.com/flex035/user_avatar/discourse.elm-lang.org/jerith/32/114_2.png) [@jerith](https://discourse.elm-lang.org/u/jerith)\
**Post date:** [September 21, 2025, 3:31pm UTC](https://discourse.elm-lang.org/t/solving-elm-router-double-update-problem/10436/3 "2025-09-21T15:31:33Z")

</div>

This seems very analogous to the `expectedUrlChanges` counter in the `elm-route-url` package ([updated for 0.19](https://github.com/rgrempel/elm-route-url/pull/38)). Specifically, you can look at [the `update` function](https://github.com/jerith666/elm-route-url/blob/update-019/src/RouteUrl.elm#L532), where it’s incremented and decremented based on whether we’re handling an expected `onUrlChange` versus some `Msg` from the app that wants to initiate a new change to the URL.

That package might be overkill for what you need, but I found it really simplified things for me to have this stuff managed by a dedicated library.

---

<div class="post-metadata">

**Author:** ![paulh](https://yyz1.discourse-cdn.com/flex035/user_avatar/discourse.elm-lang.org/paulh/32/5022_2.png) [@paulh](https://discourse.elm-lang.org/u/paulh)\
**Post date:** [September 21, 2025, 10:22pm UTC](https://discourse.elm-lang.org/t/solving-elm-router-double-update-problem/10436/4 "2025-09-21T22:22:20Z")

</div>

> [@rinn7e](#):
>
> The idea is to keep track of a boolean flag called `isInternal` that indicates whether the URL change originated from inside the app or from an external action such as the browser’s back/forward buttons.

Hi,

I think you’re overcomplicating things here, and don’t need to track an `isInternal` flag.

When you trigger a new URL with `Nav.pushUrl` your `update` function gets called with `UrlChanged` which is where you can run your expensive `parseAppRoute` function - you shouldn’t need to run it in the `ClickedLink` branch, can’t you just push the new url, and then `parseAppRoute` in the `UrlChanged` branch?

I think where you’re also coming unstuck is batching `Cmd`s with the `Nav.pushUrl` cmd. Do you really need to run these extra cmds before `UrlChanged`?

Here’s and example from a current app where the heavy lifting is only done once the url has changed:

```elm
ClickedLink urlRequest ->
    case urlRequest of
        Browser.Internal url ->
            ( model
            , url
                |> Route.fromUrl
                |> Route.pushUrl (Session.navKey model.session)
            )

        Browser.External href ->
            ( model
            , Nav.load href
            )

UrlChanged url ->
    model.page
        |> Page.urlChanged url
        |> Tuple.mapBoth
            (\newPage -> { model | page = newPage })
            (\pageCmd -> Cmd.map GotPageMsg pageCmd)

-- Page.urlChanged

urlChanged : Url -> Model -> ( Model, Cmd Msg )
urlChanged url model =
    let
        toRoute =
            Route.fromUrl url 
    in
    if toRoute == model.currentRoute then
        ( model, Cmd.none )

    else
    -- Handle any logic here relating to the route change

```

HTH

_EDIT_ Just an afterthought, if you run `parseAppRoute` on your `ClickedLink` branch, depending how expensive it is, it could cause a lag in the user clicking a link, and the url actually changing.

---

<div class="post-metadata">

**Author:** ![rinn7e](https://avatars.discourse-cdn.com/v4/letter/r/6a8cbe/32.png) [@rinn7e](https://discourse.elm-lang.org/u/rinn7e)\
**Post date:** [September 24, 2025, 12:21am UTC](https://discourse.elm-lang.org/t/solving-elm-router-double-update-problem/10436/5 "2025-09-24T00:21:24Z")

</div>

Yep, I actually used eml-route-url during elm 0.18, and was quite surprise that 0.19, it stops working. So my mindset about routing was always like that.

---

<div class="post-metadata">

**Author:** ![system](https://canada1.discourse-cdn.com/flex035/uploads/elm_lang/original/1X/50a05e53677a2c3b47776d7abd0f113eb50193a1.png) [@system](https://discourse.elm-lang.org/u/system)\
**Post date:** [October 4, 2025, 12:24am UTC](https://discourse.elm-lang.org/t/solving-elm-router-double-update-problem/10436/7 "2025-10-04T00:24:41Z")

</div>

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