React-router: Async transitions in v4

Created on 22 Dec 2016  路  11Comments  路  Source: ReactTraining/react-router

The v4 API looks amazing. Great work on it! Thanks for putting so much effort in on it.

I love the example on animated transitions, and how well it falls in with TransitionMotion. However, there's still a use case that a solution isn't immediately apparent to me with async loading.

Consider GitHub's tab navigation: When I click on a link, a pushState is immediately triggered to modify the location, and a loading bar on the top is triggered. The contents of the page, however, remain unchanged until async operations complete.

In v4, we have declarative Matches:

<Match pattern='/issues' ... />
<Match pattern='/pull-requests' ... />

Match has the flexibility to render non-exclusively with siblings, which is wonderful. Having it be exclusive in situations like this could be helpful, in order to render the previous Match before rendering a Match when some state is ready.

My hour-or-so of pondering lead me to two possible solutions so far:

1) Enhance Match with an transitionKey prop which assigns it to an exclusive set. When a pattern match occurs, it renders the new Match, providing the previous Match's children. Maybe something like this:

<TransitionMatch 
  transitionKey='tabContent' 
  pattern='/issues' 
  component={someState.isIssuesLoaded ? <Issues /> : <PreviousMatch />} />

<TransitionMatch 
  transitionKey='tabContent' 
  pattern='/pull-requests' 
  component={someState.isPRsLoaded ? <PullRequests /> : <PreviousMatch />} />

2) Wrap exclusive Matchs in a component which does the same kind of thing as above.

I feel both of these could be done outside of react-router core, but I'd love to hear any thoughts you may have had on this type of use case.

Somewhat related: #4287

All 11 comments

IMHO, the change of the URL should happen after the data for that view is loaded.

I do not think what Github does is the best model.
If you take any non SPA site, the URL changes when the new page starts rendering.
Check for example: https://archive.org/

However if we are smoothly transitioning from a page to another I think that the time to modify the URL is before the animation.

So I think the ideal scenario would be:

Link is clicked  Data starts loading     Transition starts   Transition ends
|----------------|-----------------------|-------------------|---------------- - - -  -   - 
                                         URL changes

The problem is keeping a consistent behavior when you use the pushState to change route, since the native browser behavior is to change immediately the URL.

When I was not using the react router, beside using something similar toLink, I used a function in React context to change route instead of the bare pushState.

@ChrisCinelli I initially disagreed, but then inspecting the behavior of browsers with normal links, I think I'm on your side of the fence now.

I like that the code of react-router v4 is really quite small. Prototyping ideas against it doesn't seem too intimidating. Perhaps we can try to extend BrowserRouter or Match/Link so that this advanced use case is possible?

For me, this use case is really important--I want to make my current app transition with a great UX. I may spend some time today playing around with ideas against v4. I hope I'm not reinventing ideas in the works by any collaborators!

Thinking about it more, part of v4's elegance is its declarative aspect--the component tree, including Match/Miss, are a representation of the current URL. If data loading is colocated with components, such as the <Match pattern='/issues' /> example, it's only sensible for that the URL represent that before any of the component loading can occur.

Without the URL representing the intent, that transitional state needs to be kept, and it increases complexity.

I think the best solution is a TransitionMatch component that holds state of potential previous children until a promise is resolved. I might give an implementation a whirl in the next day or two as I avoid the holidays =P

@ChrisCinelli Also, as it stands, Router reacts to programmatic changes in the URL that are done via history. So, if something outside of <Link> changes the location, we would need somehow to trigger the data loading logic of matched components before the router knows the new URL state.

It seems like too much complexity for part of the UX that users don't notice much.

I did a little bit of coding yesterday, but I ended up needing to fork Match instead of compose around it; I need access to the internal path pattern matching.

Practically speaking, I'm not going to devote too much time on this issue until the direction of v4/v4-alt settles down. Since there's uncertainty in the API and internals, anything I do now could be for nothing, especially since I can't yet use v4 on my client project. Really excited for when it hits beta, though!

Thanks for bringing this up, @quicksnap. v4 is starting to settle down now, so if you'd like to take a look that'd be great.

I believe our recommended strategy going forward will be to do async stuff before you navigate, as @ChrisCinelli described.

I used to think that the router needed to be resilient enough to load data whenever the URL changed, i.e. you would do your data loading after the URL changed, just in case someone changed it e.g. without using a <Link>. However, after pursuing that route for the past few years I think a more powerful/flexible solution would be to initiate the data fetch immediately when the link is clicked, and update the URL when you're done. With our component-based API, this would be as simple as wrapping <Link> into your own custom <DataFetchingLink>.

Also, with the prominence of global state sharing libraries like Redux, this pattern becomes a little more feasible. Click the link, trigger a "load the data" action, navigate. Anyone who is subscribed to the global state can show a spinner or do whatever they need to show that a request is in progress. At least that's the way I'm handling it in stuff I build these days.

Hopefully that helps :)

Thanks for the info, and glad to hear it's settling down!

I'll try taking a look soon if I can get some time. There may be some internal stuff (route matching) that needs to be exposed some way, but I'll try to avoid that.

Thanks for all your work. I _really_ like the direction of v4 and am excited to see it come together.

It's entirely possible I am missing a huge amount of context, but here goes:

If async data loading is done when you click on a link (before the URL changes), then how do you deal with people that come in from links on external sites, or bookmarks, or reload the web page?

@faassen Just check in the cWM of your route component for if the state is already loaded, and if not, fire off a data loading event/action/function/etc.

if (!this.props.myData.loaded) this.props.myData.load()

@mjackson
I'm still confused about your idea of wrapping Link to solve async navigation. Just consider the two cases:

  1. The user changes the URL by using browser's back/forward buttons.
  2. The user changes the URL directly by typing in the address bar.

In both cases, the URL is changed without any data fetching and the solution falls apart.

The desired behaviour would be the one similar to what Github does:

  1. Async loading begins no matter how we changed the URL, the UI remains untouched.
  2. When the data is ready the UI updates.

So far I can't see any other way but write my own customized implementations of Switch/Route. Any thoughts?

To sum up, I ended up creating AsyncSwitch component that wraps Switch.
At high-level it looks like this:

<AsyncSwitch>
  <AsyncRoute onLoad={loadPage1Data} path="/page1" component={...} />
  <AsyncRoute onLoad={loadPage2Data} path="/page2" component={...} />
</AsyncSwitch>

The idea is that AsyncSwitch changes the active route only when the corresponding onLoad method (that returns a promise) has completed. Basically, I just delay updating props.location down the hierarchy until the loading is done. AsyncRoute here is just a wrapper that extends PropTypes to accept the onLoad method.

This solution is definitely far from perfect. For one thing, I had to copy some of the Switch.render logic to work out which onLoad method I should call when the location changes. Not to mention that AsyncRoute components can't be nested deeper.

I haven't used previous versions of ReactRouter (v3 and earlier) and must have stepped into some pitfalls while implementing all this, so any feedback is appreciated.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

maier-stefan picture maier-stefan  路  3Comments

sarbbottam picture sarbbottam  路  3Comments

andrewpillar picture andrewpillar  路  3Comments

ryansobol picture ryansobol  路  3Comments

hgezim picture hgezim  路  3Comments