React-apollo: Synchronously re-mounting a graphql-wrapped component gets it stuck on `loading`

Created on 18 May 2017  路  13Comments  路  Source: apollographql/react-apollo

This seems related to #170, but I think it'll be something much simpler to solve.

Steps to Reproduce

Here's a sample project: https://github.com/dallonf/react-apollo-bug-sync-remount

In a nutshell, a graphql-wrapped component is rendered. On user input, the component is unmounted and replaced by another component that immediately, in componentDidMount, triggers a re-render that causes the graphql component to be re-mounted.

In real-world usage, this type of scenario can be caused by React Router's <Redirect /> component, for example.

Buggy Behavior

The graphql-wrapped component's data.loading prop will be stuck on true, even though the network request has been completed, and the query is correctly updated in the Redux store.

Expected Behavior

The graphql-wrapped component should behave as it did on its initial mount, where loading will eventually be set to false and its data correctly updated.

Some more research

The difference between a "stuck" component and a working one seems to be that the APOLLO_QUERY_STOP event triggered when it mounted, which effectively resets the recycled query. I'm not sure exactly why this event is so important, but I do know roughly why it isn't being called.

This event is triggered when you unsubscribe from a query; specifically, the tearDownQuery call in the ObservableQuery.prototype.onSubscribe() method. This only happens when there are _no more observers_ on the query. In this scenario with a synchronous re-mount of a component, there is no point where there are no observers. Here's what happens instead:

  1. Component mounts, triggering an observer, let's call it subscribe1. There is now 1 observer active.
  2. Component loads its data, everything is fine
  3. Component unmounts
  4. graphql starts to recycle the query, calling setOptions, which in turn calls setVariables and result, which ultimately sets up an observer. Let's call it setOptions. There are now 2 observers active.
  5. As part of the recycling process, a dummy observer is also created, to keep the query alive. Let's call it recycle. There are now 3 observers active.
  6. Finally, the component unsubscribes from subscribe1 to finalize the unmounting process. There are now 2 observers active.
  7. Component re-mounts
  8. Seeing that there is a recycled query available, the component picks it up and tears down its dummy observer, recycle. There is now 1 observer active.
  9. _Uh oh! This is the part where APOLLO_QUERY_STOP should have been called, but there's still an observer active: setOptions_
  10. Component re-subscribes to the recycled query, creating an observer we'll call subscribe2. There are now 2 observers active and everything is awful.
  11. Finally, after a quick setTimeout, the setOptions observer unsubscribes, too late to save the poor component.

I'm not sure what this means. Maybe the fact that this rogue observer unsubscribes on a setTimeout is a bug. Maybe the component shouldn't rely on this APOLLO_QUERY_STOP event to reset the recycled query. Or maybe it ought to explicitly call tearDownQuery() somehow to make sure it's a clean slate. I would have messed around with finding a solution, but I couldn't get the test suite running (reported this separately in https://github.com/apollographql/react-apollo/issues/717), so I had to settle for just reporting the bug.

Version

bug confirmed

Most helpful comment

good news everyone! This will be fixed in 1.4.0 which should release tomorrow!

All 13 comments

I have a component that is mounting and unmounted by the router the same as this. I can never seem to keep in a state where it does not be loading constantly. Do you have a fix for this?

Not a fix - that's why I reported the bug. I'm currently working around it by wrapping the component in an HOC that waits a tick before actually rendering it - which is pretty nasty.

I have the same problem, when the same component is unmounted on a route leave and on the next route is mounted again it gets stuck in loading state with networkStatus 2

@dallonf thanks for the reproduction! I'll take a look and see if I can figure out what is happening.

Have the same issue. Within react router, we have a component that we use within different routes, when user navigates from one to another, remounting the component it results with loading: true indefinitely.

+1 I also have the same issue with 100% reproduction rate!

This seems to be also discussed at #667 which has some recent activity regarding to the source of issue and possible solution. Also see #678

This looks like a major issue so I hope there would be a release soon.

good news everyone! This will be fixed in 1.4.0 which should release tomorrow!

Good one James!

Unfortunately this still hasn't resolved the issue for me. Upgraded apollo-client to 1.4.0, I have "react-apollo": "^1.2.0", . The issue, as described, occurs when unmounting / remounting the same component on different routes. Also happens only on first change of the variables, if I go back and forth to this page using browser back button, it renders correctly. I receive the data from server correctly, APOLLO_QUERY_RESULT also updates network status to 7, the same happens in APOLLO_QUERY_RESULT_CLIENT on second try and then it works. Did the new version solve it for anybody else?

@hizo Should we wait for this PR (https://github.com/apollographql/react-apollo/pull/740) to be merged?

hmm looks like it. hopefully it will happen soon

This is now fixed via 1.4.0! You will need to update apollo-client to the same version!

Was this page helpful?
0 / 5 - 0 ratings