redux-toolkit
redux-toolkit copied to clipboard
RTK Query gets stuck in "pending" state when following the persistence example from the website
Problem description
When you set up your store to persist your api reducer as outlined on the website, there's a chance your persisted queries get stuck in "pending" state if the app gets interrupted while fetching a query (e.g. the window is reloaded.) This only happens when a few conditions are met:
- You've set up redux-persist to persist the api reducer, not the root reducer.
- None of the queries that get rehydrated are fulfilled or rejected, e.g. they were all pending when the app got interrupted
- You're using
autoMergeLevel1as state reconciler for redux persist - You're using
extractRehydrationInfoin your api slice as documented on the website
When both those condition are met, the following will happen upon rehydration:
- The app loads, state is initialized, etc.
- The current state of your api slice looks a bit like this:
{ queries: {}, mutations: {}, config: {...} } - redux-persist's
REHYDRATEaction is triggered - Our
extractRehydrationInforuns and returns theREHYDRATEpayload to RTKQ - RTKQ takes that payload and sets all queries that are either
fulfilledorrejectedon thequeriesobject. In our case that's none of them (condition 2 above.) This happens here: https://github.com/reduxjs/redux-toolkit/blob/24286f1745e42217448de1f3486886fa8558c1b5/packages/toolkit/src/query/core/buildSlice.ts#L390-L399 - At the end of
REHYDRATE, redux-persist will useautoMergeLevel1to do a shallow compare between the state before and after REHYDRATE. It will not detect any changes to thequeriessubkey of the state at this point and resort to retaining what it loaded from storage (i.e. the pending queries.) This happens here: https://github.com/rt2zz/redux-persist/blob/d8b01a085e3679db43503a3858e8d4759d6f22fa/src/stateReconciler/autoMergeLevel1.ts#L24 - RTKQ will not load the data for those queries anymore because (since they're in pending state) it's waiting for them to complete which they will never do.
Example
I've set up a minimal example that shows the issue here: https://codesandbox.io/p/sandbox/qpv4m9
Inside the getPokemonByName endpoint, it does a window.reload() to simulate the user reloading the page. So if the code worked, the example would be in an infinite reload loop. Instead it will rehydrate the pending query from localStorage after the first reload and get stuck there.
Towards a solution
I don't think there's a bug in redux-persists, nor in RTK Query. Both work as advertised by themselves.
The problem is that the example code on the RTK website on how to make them work together doesn't work in all scenarios. I can think of two ways to resolve this:
- Using a custom state reconciler, I think it could be as simple as writing one that always uses the so-called
reducedState(i.e. just use whatever is returned by the api reducer duringREHYDRATE.) - Writing a transform for redux-persist that doesn't persist these pending queries in the first place
I'm happy to help out on either one but I'd be interested in hearing the team's thoughts on this first.
- I'm assuming you would like to prevent having any kind of
redux-persist-specific code in the library? - Do you see this as something that gets written out fully in the documentation page on the website for people to copy-paste?
- Would you be willing to either maintain or link to a separate transform package for redux-perist? (e.g.
redux-persist-transform-rtk-querylike the others they list on their README by independent developers.)
Yeah, persistence is outside the scope of RTK, so better documentation is the best answer here.
Do you have a preference on which of the solution you'd like to see in the documentation?
I'll add a bit of example code and pros and cons to get started. It will be a longer post again, sorry in advance 😅.
Using transforms
- Pro: You're not perisisting data you won't use anyway
- Con: It's a bit more complicated to (a) understand and (b) document in its entirety
- Con: The implementation is significantly different when persisting the api reducer then when persisting the root reducer, increasing the length and complexity of the docs
- Con: Consumers of RTK-query are adding code to their application that (to me at least) feels a bit like playing with RTKQ's internals (i.e. knowledge about the structure of its state object)
The first two cons could be countered by either:
- Adding a dedicated page for this to the docs, or
- Releasing a separate
redux-persist-transform-rtk-querypackage and documenting its use, abstracting away most of the complexity from the documentation
The last con could be countered by a separate package too, provided the package has a trusted and active maintainer (which is why I suggested releasing that under the reduxjs namespace, but I'm sure Ambassify would be willing to releasing it too if desired.)
Note: While it won't hurt to add it, it is no longer required to use extractRehydrationInfo.
Some code illustrating what we did at Ambassify (for persisting the api reducer.)
import { pickBy } from 'lodash';
import { createTransform } from 'redux-persist';
import storage from 'redux-persist/lib/storage'
import { QueryStatus } from '@reduxjs/toolkit/query';
/**
* We only want to persist fulfilled queries. Caching anything else doesn't
* make much sense and can even cause bugs: https://github.com/reduxjs/redux-toolkit/issues/4715
*/
const whitelistFulfilledQueries = createTransform(
(inboundState: any) => pickBy(inboundState, { status: QueryStatus.fulfilled }),
(outboundState: any) => pickBy(outboundState, { status: QueryStatus.fulfilled }),
{ whitelist: [ 'queries' ] }
);
/**
* Keep all provided mappings between queries and the invalidation-tags
* (shortcut taken, didn't look into filtering out tags that become irrelevant
* because their queries would be dropped, that was a bit too much effort.)
*/
const whitelistProvidedTags = createTransform(
(inboundState: any) => inboundState,
(outboundState: any) => outboundState,
{ whitelist: [ 'provided' ] }
);
/**
* We don't want to persist mutations, subscriptions, config, ... The cache is
* only used when the application loads so none of those are useful at that point.
*/
const removeNonQueries = createTransform(
() => ({}),
() => ({}),
{ blacklist: [ 'queries', 'provided' ] }
);
const persistConfig = {
key: 'root',
storage,
transforms: [
whitelistFulfilledQueries,
removeNonQueries,
]
}
Using a custom state reconciler
- Pro: The code for this is a lot less complicated
- Pro:
extractRehydrationInfostill hides the RTKQ internals - Pro and con: It's only required for users that want to persist the api reducer, so while the docs do need to explain that complication, it is still only more work for some users
- Con: It persists data the user will never need or use (since entire state is persisted)
- Con: I'm not 100% sure it works properly, as we've only briefly tested this before choosing the transforms solution internally
import storage from 'redux-persist/lib/storage'
function rtkqStateReconciler(inboundState, originalState, reducedState) {
return reducedState;
}
const persistConfig = {
key: 'root',
storage,
stateReconciler: rtkqStateReconciler,
}
I'm facing the same issue. I think it's a major issue that need to be fixed because it's causing a lot of problems in production.