redux-toolkit icon indicating copy to clipboard operation
redux-toolkit copied to clipboard

Selectors stuck in isLoading state on inactive tabs despite successful data fetch

Open cnt-null opened this issue 8 months ago • 9 comments

Hi everyone, I'm hoping someone can help me understand and resolve a strange behavior I'm encountering with rtk-query on inactive browser tabs.

Data fetching occurs on inactive tabs, the data arrives from the server, and even queryFulfilled is triggered. However, the selectors in the components don't seem to react, as if the query is still in an isLoading state.

I've created a test project to demonstrate this: https://github.com/cnt-null/rtk

Steps to reproduce:

  • npm ci
  • npm run dev
  • Open localhost:{port}
  • command + click on "bulbasaur"
  • Hover the cursor over the title of the new inactive tab - it will show "Vite + React + TS".
  • Wait for a short period, for example, a minute.
  • Open this inactive tab.
  • The title of this tab will change to "ivysaur".
  • Check the console.log output for this tab. You can observe the delay (e.g., a minute) between "query fulfilled" and "useQuery data"

I've tried various methods to force a fetch, but none of them have helped.

cnt-null avatar Apr 22 '25 09:04 cnt-null

I cloned the project and I do agree I can see that behavior. My first suspicion is that this may be due to Chrome throttling background tabs in some way.

Overall I would be very surprised if there's anything in React-Redux or RTK that is actually an issue here. We just rely on React's useSyncExternalStore, which subscribes to the store, runs the selectors, and queues React re-rendering.

Don't have time to look into this further, but some questions that might help to look at this:

  • Does this happen if you use any other browser?
  • Does this happen with another library like React Query, or any other async logic loading on startup?
  • Does it happen with any other use of useSyncExternalStore?

markerikson avatar Apr 26 '25 22:04 markerikson

Ohhhh, waitasec.

I went in to the RTKQ bundle in node_modules and added some logging to the query selectors.

I see this output:

hook.js:608 Sat Apr 26 2025 18:42:16 GMT-0400 (Eastern Daylight Time) 'Running selector output: ' 'getPokemon(2)' 
Object
overrideMethod	@	hook.js:608
(anonymous)	@	buildSelectors.ts:139
recomputationWrapper	@	createSelectorCreator.ts:392
memoized	@	weakMapMemoize.ts:228
dependenciesChecker	@	createSelectorCreator.ts:412
memoized	@	weakMapMemoize.ts:228
selectFromState	@	buildInitiate.ts:283


Sat Apr 26 2025 18:42:28 GMT-0400 (Eastern Daylight Time) 'Running selector output: ' 'getPokemon(2)' 
Object
overrideMethod	@	hook.js:608
(anonymous)	@	buildSelectors.ts:139
recomputationWrapper	@	createSelectorCreator.ts:392
memoized	@	weakMapMemoize.ts:228
dependenciesChecker	@	createSelectorCreator.ts:412
memoized	@	weakMapMemoize.ts:228
collectInputSelectorResults	@	utils.ts:122
dependenciesChecker	@	createSelectorCreator.ts:405
memoized	@	weakMapMemoize.ts:228
collectInputSelectorResults	@	utils.ts:122
dependenciesChecker	@	createSelectorCreator.ts:405
memoized	@	weakMapMemoize.ts:228
(anonymous)	@	buildHooks.ts:1111
(anonymous)	@	useSelector.ts:171
memoizedSelector	@	use-sync-external-st…r.development.js:57
(anonymous)	@	use-sync-external-st…r.development.js:70
checkIfSnapshotChanged	@	react-dom-client.development.js:6116
(anonymous)	@	react-dom-client.development.js:6109
(anonymous)	@	Subscription.ts:29
defaultNoopBatch	@	batch.ts:3
notify	@	Subscription.ts:26
notifyNestedSubs	@	Subscription.ts:124
handleChangeWrapper	@	Subscription.ts:129
wrappedListener	@	autoBatchEnhancer.ts:79
(anonymous)	@	createStore.ts:220
dispatch	@	createStore.ts:219
s	@	page.bundle.js:3
dispatch	@	autoBatchEnhancer.ts:112
(anonymous)	@	index.ts:70
(anonymous)	@	serializableStateInv…ntMiddleware.ts:169
(anonymous)	@	redux-thunk.mjs:7
(anonymous)	@	immutableStateInvariantMiddleware.ts:174
(anonymous)	@	actionCreatorInvariantMiddleware.ts:29
dispatch	@	page.bundle.js:6
handleFocus	@	setupListeners.ts:33
handleVisibilityChange	@	setupListeners.ts:39

Notice that the second hit's call stack traces back to RTKQ's handleVisibilityChange. That was added from the setupListeners(store) method in the store.ts file, and it controls turning off queries if the tab isn't focused:

  • https://redux-toolkit.js.org/rtk-query/api/setupListeners

If you turn off that line, RTKQ should go ahead and fetch in the background even if the tab isn't focused.

markerikson avatar Apr 26 '25 22:04 markerikson

@markerikson thanks for responding and taking a look.

It seems that handleVisibilityChange triggers for the first time when an inactive tab becomes active. I tried commenting out handleVisibilityChange in the custom handler. I also tried dispatching onFocus() initially in the custom handler without any "if" conditions, so as not to wait for the tab to receive focus. I also completely removed the line with setupListeners(store). But the situation hasn't changed.

It reproduces in Chrome and Safari. But, surprisingly, everything works in Firefox (without any edits to setupListeners). So, it really is about specific browser behavior.

As a workaround, after queryFulfilled I dispatch a dummy action (basically, any action), and the component re-renders, updating the inactive tab's title.

cnt-null avatar Apr 28 '25 10:04 cnt-null

@markerikson, this still happens in v2.8.2. To test, I created sandboxes for both RTK Query & React Query, and it only happens with RTK Query.

martynasgz avatar Aug 01 '25 11:08 martynasgz

@martynasgz : can you link the RTKQ sandbox? Not sure why a React Query sandbox would be relevant - that's an entirely separate library, completely different implementation.

markerikson avatar Aug 01 '25 16:08 markerikson

@martynasgz : can you link the RTKQ sandbox? Not sure why a React Query sandbox would be relevant - that's an entirely separate library, completely different implementation.

Hey, thank you for your quick response. I mentioned React Query because of your previous message, asking to compare behavior:

Does this happen with another library like React Query, or any other async logic loading on startup?

To reproduce, you can use the same sandbox @cnt-null gave. In my particular case, I poll until I get a specific result. It works well until the tab loses focus - then it continues polling infinitely, seemingly without ever selecting the result. I can recreate my case too, if needed.

martynasgz avatar Aug 01 '25 16:08 martynasgz

@martynasgz my conclusion looking at the earlier sandbox was that it was expected behavior given the specific app settings they have. If you can provide yours, that would help see if there's anything different going on.

markerikson avatar Aug 01 '25 16:08 markerikson

@markerikson apologies for the late reply, here's the sandbox I quickly drew up from the React Query one I had: https://codesandbox.io/p/devbox/blissful-shape-kxsxy4. It polls until the response array includes the number 10. However, if the tab is unfocused, it continues polling past the number 10, and finishes only when you focus the tab again.

martynasgz avatar Aug 03 '25 16:08 martynasgz

Hmm. Is this possibly related to the autobatching and requestAnimationFrame usage as discussed in #5051 ? What happens if you change the default timer option, like this?

  const store = configureStore({
    // ...
    enhancers: (gde) =>
      gde({
        // defaults to `"raf"` for `requestAnimationFrame`, try `"tick"`, `"timer"` or `false`
        autoBatch: false,
      }),
  })

markerikson avatar Sep 03 '25 01:09 markerikson