Routing when you already there

How do you handle situations like this:

Your application navigates to some route that involves loading some data. Lets say its a folder that may contain some images.

Browser.Navigation.pushUrl key "/myapp/folder/1/"

results in

Browser.application.onUrlChange being called. The URL is parsed to get a new route, and this triggers the next step, which is to see what is in the folder.

An HTTP request is made to fetch the contents of folder 1. There may be no images in it, but in this case a list of image files and their URLs comes back. In this particular application, if there are no image files we just want to list what files are in the folder (maybe some non-image files), but if there are images, we want to show the first one. The Model is updated to reflect that a single image is to be shown, and the view displays it. Since we are now on a particular image, we want to update the URL to reflect that (in case the user bookmarks it):

Browser.Navigation.pushUrl key "/myapp/folder/1/holliday-snaps.jpg"

results in

Browser.application.onUrlChange being called. The URL is parsed to get a new route, and this is different to the folder route we were on previously. This application reacts to this route change by loading the image again, even though that is the state we are already in.

In other words, I just want to change the URL, and not fire an event that will trigger whatever side-effects the application generates when switching to a new route.

Seems like one answer is that I should store the current route in the top-level Model, change that to ViewImage (Folder 1) "holliday-snaps", then push that URL. In onUrlChange compare the new route to the current one, find that there is no change, and fire a Noop to ignore it.

Sometimes feels like there should be additional options to Browser.Navigation.pushUrl/replaceUrl that simply sets the URL in the browser, but does not trigger a navigation event to Browser.application.onUrlChange.

I feel this area of Elm applications can easily get a bit muddled, and interested to hear how others deal with this to ensure that the URL is always accurate and changes to it do not result in duplication of side-effects.

3 Likes

Thinking about this some more, I realised my example works better the other way around. Suppose a user ends up at /myapp/folder/1/holliday-snaps.jpg, and bookmarks it because it is a nice picture. Later on they return to this bookmark, but the picture has been deleted. The application responds to a 404 on an image in a folder by showing the rest of the folder of images as thumbnails so they can perhaps choose something else. The URL for the folder would be /myapp/folder/1/.

The normal sequence of events is that the user or application navigates to a URL, and then the application responds by setting itself up in the correct state to display the requested information.

Sometimes something happens, and the application cannot achieve the originally requested state. It shows a different state instead. If that state has a particular URL that describes it well, it is helpful to update the URL to that (possibly also adding it to the browsers history). In this case, it is not the application state that follows from the URL, but the URL that follows from the different application state chosen.

That update to the URL should not trigger a repeat of the work needed to get to that state, since the application is already in it.

The question is how do you tie the application state and route together such that one can follow from the other in either direction? It seems like some de-duplication is required, either on the routes or on the application state, where is best to do that? Or perhaps it is possible to always arrange that the flow in an application is always unidirectional (route → state and never state → route)? Examples or thoughts appreciated. :thinking:

I don’t think this issue is particularly unique to Elm. It comes up lots of front-end frameworks. IMO, if fetching data is your bottleneck (and it often is), setting even fairly short cache times on the HTTP responses can go a long way to reducing duplicate network activity, even if the app thinks it’s making requests from scratch each time. Then you don’t have to worry about whether route is driving state or vice versa.

There are lots of front-end frameworks that have libraries for caching responses to XHR requests in JS memory rather than the browser cache. I’m not generally a fan of that approach, but lots of other people seem to be.

It seems like maybe you’re also trying to get at a more general question, but maybe this is a case where generalizing the problem only makes it harder, and it’s better to just solve specific cases for specific apps/features?

We ended up applying a simple rule: All state changes and side effects that are linked to a url should be a reaction to the url change. Then, in the image example, just push the new url, and wait for Elm to react to this change. Many times we move side effects calls from update to init of a page because we exposed them as query string arguments, which means that they have become linked to the url. This rule has clarified and simplified the scenarios that before were a bit messy.

Then, we also make the check for duplicate urls a responsibility of each page because sometimes you might want to allow it and sometimes not.

1 Like

In our framework we default to the load the world approach - i.e. a url change would call the page’s init function that would presumably load stuff from scratch (although our framework does support caching which can also mitigate this problem).

However, for cases like this a page can opt in to instead receive a onFlagsChanged message which is meant for situations akin to this (for us for instance if a user types into a search field, we want to persist the search in the url, but don’t want to reload the whole page on every keystroke).

We call it onFlagsChanged since our framework decides which page to show based on a function matchRoute : Route -> Maybe flags, where flags is sent as the input to a page’s init function, and the framework loads the first page that evaluates to Just.

Hence we detect this situation by checking if the matchRoute oldRoute evaluates to Just x, and matchRoute newRoute evaluates to Just y and x /= y, then we can alert the page to this condition.

The particular issue I have (which is in a propriatary codebase not the example folder-of-images-app described above) is not that I am worried about making the same HTTP request more times than needed - It is that the application has an issue reliably creating its state from the URL, if the same state is initialized more than once! It is a bit of a difficult one to tackle, as the state is spread out in quite a few places, but it is essentially an issue of being able to represent illegal states.

I was able to fix it by writing some JS code on a port, and calling it Navigation.silentReplaceUrl. As you can guess from the name, this sets the browser URL, but does not trigger a navigation event back to Browser.application.onUrlChange.

This is a tactical fix that will do for now, but it feels hacky. The proper fix will be to sort out the illegal states and latching issue that happens during init (its a bit like latching during a reset in digitial circuit design isn’t it?)

Yes, I’ve now seen the wrong way of doing it, so I got curious about what is the correct way, if I were writing another application from scratch. I think handling specific cases without sight of what principles could be followed to ensure that navigation always works cleanly, is probably a bad idea, and the kind of thinking that lead to my problems in the first place.

@francescortiz , @gampleman - you both seem to be advocating the forward only, url -> state approach, which seems like the right principle to follow. Browser.Navigation.pushUrl always triggering an event -> Browser.application.onUrlChange also seems to bias Elm applications towards working that way.

I am thinking that checking for a duplicate route is best done on the Route and not the application Model (or page State or however you model your state)?

What if you have a route that every time you access it gives you random dice throw. Then it makes sense to allow for duplicate urls. As long that you take into account that some urls might want repetition, then where the logic sits doesn’t seem that relevant to me right now.

Something along the lines of:

type Route
    = ShowImage Folder ImageId
    | RollDice
    | ...


dedupeNavEvent : Route -> Route -> Bool
dedupeNavEvent current proposed =
    case (current, proposed) of 
        (ShowImage folder1 image1, ShowImage folder2 image2) ->
            Folder.isEqual folder1folder2 && ImageId.isEqual image1 image2 
                |> not -- Don't re-route to the image we are already showing.

        (RollDice, RollDice) ->
            True -- Always roll again

        _ -> 
            True -- All False cases should be covered above

update msg model = 
    case msg of 
        ...
        OnUrlChange route ->
            if depdupeNavEvent model.currentRoute route then
                changePageToRoute route model
            else
               (model, Cmd.none) -- Already on the requested route and in the correct state.
1 Like

I wrote a bit about this here a couple months ago:

1 Like

The “philosophically” section of the elm-route-url readme has some good discussion and links too.

1 Like

Using the URL both as a snapshot of the navigation state and as a source of truth for the navigation state looks like a bidirectional binding, which I something that I try to avoid as much as possible. In which situations do you see it as an advantage?

That philosophy section refers to a thread on the old Elm mailing list. The 2 styles are described as:

1. Allow the address bar to drive your model

or

2. Allow your model to drive the address bar

1 is the route → state scheme and 2 is the state → route scheme. I guess we could also have 3, which is when you use a mix of 1 and 2, but in fact I think that 2 necessarily means you must also do 1, so 3 is the same as 2.

It got me thinking, that 1 is the scheme which is easiest to make correct, and to maintain the correctness of. The reason for this, is that if every route change in your application requires that the application rebuilds the state from the route, and this is the ordinary way of your application working, then this route → state function will be well tested. As I have found, you can still trip yourself up if you somehow rely on hidden (latched) state, and later find out that the route → state function does not know everything it needs to.

It seems to me that scheme 2 may be more likely to contain errors. If you make a URL from the state, you may in fact create a URL that cannot succesfully re-create the state. If turning that URL back into the state is not the normal way for your application to work, but is only exercised occasionally, when the user bookmarks and returns to a URL, or hits refresh on the page. And I have also just described why scheme 2 must also include doing 1, you generate URLs from application state, but you must also support going the other way, otherwise what is the URL for?

This seems like a fairly good argument for doing 1, but I am not totally convinced of it yet.

I also note that in 2, you have to have isomrophic route → state and state → route functions. Isomorphic functions are great for property-based (fuzz) testing. You create a fuzzer for say the routes, then pass that through routeToState >> stateToRoute and check you always comes back to where you started. Its more work to correctly maintain a pair of isomorphic functions, but perhaps good testing cures that or even makes it easier?

Yeah, I think that’s all correct. The thing I like about elm-route-url is that it keeps those functions for translating back and forth between URLs and state “off to the side”, so you can mostly focus on the conceptually simpler & standard TEA update function and use your Msg type in your view – you don’t always have to be thinking about how things are going to get indirected through the URL, it’s mostly handled for you.

1 Like

More often than not not I end-up with doing the stateful management in applications I work in. Anyway I would definitely stay away from pattern of constructing tuples just to deconstruct them in pattern match. That’s rarely a good design in my experience (should be either replaced by applicative or state machine - with state and transitions).

Comparing old and existing route is definitely an option but I would stay away from function that returns Bool as it holds very little information. Since your old route is probably part of a model I would define something like updateRoute : Route -> Model -> (Model, Cmd Msg) and do the work within it. If you need to pattern match I would rather nest 2 pattern matches inside each other.

Other design I’ve used in app is to have Store holding all the data. Routing then is never guaranteed to trigger request it just calls ensure function that will check if data are already fetched or not and either load them or do nothing. This works well in cases where your pages and resources don’t map 1:1.

I think the right approach really depends on specific needs of application. That makes it a bit harder to provide general design advice but doing this correctly greatly reduces future headaches. I think rather than listening for advises you should try to design approach that fits your needs the best - you’ll be glad you did that later.

I favour the approach where the url always drives the model. If you want something to be bookmarkable I will always follow this sequence:

User interaction changes URL 
-> Elm listens for URL change 
-> Load new state based on new URL

It is predictable and simpler to work with. Sometimes you might need to load things twice, but I think you can deal with that in other ways (e.g. a cache layer). Are http requests very expensive here?

In this case if you arrive at /myapp/folder/1/holliday-snaps.jpg and is a 404. Maybe the app should show a message with a link. From a user’s perspective is disconcerting that you are just redirecting to a different page without explanation.

Upon redirecting to /myapp/folder/1/ it would make a new load.

1 Like

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