T
TanStack•2y ago
foreign-sapphire

Component not re-rendering on removeQueries()

I have a FlatList with items. Lets call them FlatListItem. I am using memo to memoize my Items to prevent them to re-render when the parent changes using export default memo(FlatListItem); For each FlatListItem in my list I have to fetch data. In a separate file I define a hook and a query function. Simplified they are looking like this:
const getData = async (id) => {
let response = await axios.get(MY_URI+'/'+id);

return response.data;
};

export const useGetDataQuery = (id) => {
return useQuery({
queryKey: ["my_key", id],
queryFn: () => getData(id),
staleTime: Infinity
});
};
const getData = async (id) => {
let response = await axios.get(MY_URI+'/'+id);

return response.data;
};

export const useGetDataQuery = (id) => {
return useQuery({
queryKey: ["my_key", id],
queryFn: () => getData(id),
staleTime: Infinity
});
};
In my FlatListItem I will then use useGetDataQuery:
const { isSuccess, data } = useGetDataQuery(item.id);
const { isSuccess, data } = useGetDataQuery(item.id);
When the FlatListItem is loaded for the first time it all works as expected. Using !isSuccess I can show a loading indicator and when loading is done my data is shown. Now in a different part of my application I want to invalidate the data and trigger a refetch in FlatListItem. Again, with a loading indicator and when loaded the data should be shown. I tried to accomplish this by using removeQueries:
queryClient.removeQueries({
queryKey: ["my_key"],
});
queryClient.removeQueries({
queryKey: ["my_key"],
});
However this is not updating my component. The component will not re-render after the query cache is removed. I am missing something? How can I properly trigger a re-render of my FlatListItem?
45 Replies
solid-orange
solid-orange•2y ago
you want resetQueries, not removeQueries
foreign-sapphire
foreign-sapphireOP•2y ago
This gives me the same issue. The data is indeed invalidated, but the FlatListItem is not re-rendering and thus no new data is fetched.
solid-orange
solid-orange•2y ago
resetQueries resets a query to its initial state, then informs active observer about that change, which will re-render them and put them into hard loading state. if you see a refetch, that means it worked already because the re-rendering is what triggers the fetch. resetQueries itself doesn't trigger a refetch
Again, with a loading indicator and when loaded the data should be shown.
on a different note: why not just use the isFetching flag to show a loading indicator ?
foreign-sapphire
foreign-sapphireOP•2y ago
As I was only using isSuccess and data the components would not rerender on resetQueries as that does not reset the status and both stayed the same. removeQueries completely removes the cache and thus resets the status back to pending, but does not inform observers?
solid-orange
solid-orange•2y ago
resetQueries resets to the initial state. unless you are using initialData, that is status: 'pending', data: undefined I still don't understand what you are trying to achive. Yes, removeQueries does not inform observers. It's not meant to remove something from the cache that is actively used.
foreign-sapphire
foreign-sapphireOP•2y ago
In that case I should see isSuccess as false? Because so far what I see is that using resetQueries works for my case, but isSuccess stays true
const { status, data } = useGetDataQuery(item.id);
console.log("Render FlatListItem", item.id, status);
const { status, data } = useGetDataQuery(item.id);
console.log("Render FlatListItem", item.id, status);
This will for some reason always show 'success' after running resetQueries. isFetching will change from to true and later back to false, but the pending state is never seen. I can't figure out why.
solid-orange
solid-orange•2y ago
after resetQueries, isSuccess should definitely not be true, unless you use initialData . Should be easy to show in a sandbox if that doesn't behave like that please
foreign-sapphire
foreign-sapphireOP•2y ago
After some more testing it seems to be related to PersistQueryClientProvider with AsyncStorage. If I change that to a normal QueryClientProvider it is working as expected.
foreign-sapphire
foreign-sapphireOP•2y ago
I have managed to create a working minimal example showing the issue: https://github.com/rklomp/query-async-test
GitHub
GitHub - rklomp/query-async-test
Contribute to rklomp/query-async-test development by creating an account on GitHub.
foreign-sapphire
foreign-sapphireOP•2y ago
When I use expo 49 which uses react-native 0.72.6 there is no issues.
# npm install expo@49
# npx expo install --fix
# npm install expo@49
# npx expo install --fix
solid-orange
solid-orange•2y ago
so it's a rect-native / expo issue ?
foreign-sapphire
foreign-sapphireOP•2y ago
Disregard this, its also present using expo 49 and react-native 0.72.6. Switching to a different expo version installs a new version of expo go and thus clearing async storage. On the first app start the issue is not present, but restarting the app the issue is there. Running AsyncStorage.clear(); at the first line of App() also "fixes" the issue. Could it be that PersistQueryClientProvider is loading the initial state from storage and not really resetting the initial state?
solid-orange
solid-orange•2y ago
hm, maybe data restored from persistence is treated as initial data, I'd need to check. I'd appreciate a quick codesandbox without react-native because it's easier to debug in teh browser where we also have the devtools available.
foreign-sapphire
foreign-sapphireOP•2y ago
I am only familiar with working with react-native, but will see if I can make something https://codesandbox.io/p/sandbox/query-async-test-rqf58n After running one time. Do a refresh and you will see
Render MySubComponent success fetching
Render MySubComponent success idle
Render MySubComponent success fetching
Render MySubComponent success idle
The first line success should read pending If you remove the REACT_QUERY_OFFLINE_CACHE and refresh, it is working as expected
Render MySubComponent pending fetching
Render MySubComponent success idle
Render MySubComponent pending fetching
Render MySubComponent success idle
solid-orange
solid-orange•2y ago
thank you. I will try to take a look if there's something wrong with the combination of persisters and reset
foreign-sapphire
foreign-sapphireOP•2y ago
Thanks! I have been testing a bit more. It looks like the whole initial data is the data from when the page was loaded and the persistor was initialized. So resetQueries will reset to that data. My code sandbox is updated with some changes and the devtools: https://codesandbox.io/p/sandbox/query-async-test-rqf58n Looks like hydrate is using QueryCache.build() to create queries https://github.com/TanStack/query/blob/main/packages/query-core/src/hydration.ts#L145-L159 This will create a new Query: https://github.com/TanStack/query/blob/main/packages/query-core/src/queryCache.ts#L111-L118 Since state is set, #initialState will be set to config.state https://github.com/TanStack/query/blob/main/packages/query-core/src/query.ts#L177
solid-orange
solid-orange•2y ago
Interesting - so queries created from persisters see data restored from there as initialData 🤔
foreign-sapphire
foreign-sapphireOP•2y ago
Yes
solid-orange
solid-orange•2y ago
Can you try removeQueries() first followed by resetQueries() ?
foreign-sapphire
foreign-sapphireOP•2y ago
I have made an issue on github as well to track yhus I am now using removeQueries and lastUpdated from my parent component to trigger a render That works But resetQueries would be cleaner
solid-orange
solid-orange•2y ago
removeQueries() followed by resetQueries() doesn't work ? ah no it does not okay I think we can add an option to resetQueries to perform a hard reset, which defaults to false, so it will do initialState per default and with a hard reset, it would do the empty loading state ... on the other hand, I also see some discrepencies between hydrate and e.g. setQueryData. with hydrate, the state becomes the initial state, wich setQueryData, it does not 🤔 Maybe @Ephem knows if it's the intended behaviour of hydration to put the state we hydrate from into intialState of the query, as that impacts the reset method.
solid-orange
solid-orange•2y ago
here is what setQueryData does: - build with empty data - then setting it, which doesn't affect the initial state https://github.com/TanStack/query/blob/b67107114ce6ccf0d38c2ab22b3088df19ec119b/packages/query-core/src/queryClient.ts#L184-L186
GitHub
query/packages/query-core/src/queryClient.ts at b67107114ce6ccf0d38...
🤖 Powerful asynchronous state management, server-state utilities and data fetching for the web. TS/JS, React Query, Solid Query, Svelte Query and Vue Query. - TanStack/query
foreign-sapphire
foreign-sapphireOP•2y ago
If you do that what would be the use of resetQuery without it setting to true? There is now a difference in behavior between whether you use a persistor or not. And the behavior is not predictable anymore as the initialState can be anything after hydration And what happens if I define initialData myself. And try a reset after an app restart? Will it ever know the original initialState?
solid-orange
solid-orange•2y ago
if you set initialData, and then call resetQueries, it will reset to that state. That is on purpose
foreign-sapphire
foreign-sapphireOP•2y ago
Also if we use a persistor and restart the app? Or will it be using the state from the app start? I'll have to test that
solid-orange
solid-orange•2y ago
sorry, I'm a bit lost now and don't understand the question fully. Is it about having a persister and initialData supplied ? oh btw combining refetch + remove works: https://codesandbox.io/p/sandbox/query-async-test-forked-gyqr6v?file=%2Fsrc%2FMyComponent.js%3A17%2C1-22%2C8
foreign-sapphire
foreign-sapphireOP•2y ago
Yes when you have both. I have updated my sandbox to show this. https://codesandbox.io/p/sandbox/query-async-test-rqf58n When setting initialData this will only work on a non-hydrated run. As soon as you refresh the page and the cache is loaded from state, the initialData is gone.
solid-orange
solid-orange•2y ago
yes but that makes sense to me. The query is first created by the hydration, and queries created by hydration will (with the current implementation) get that set as initialData. Then, when useQuery comes and sends new initialData, the query already exist, and since initialData is only taken into account at query creation time, it does nothing. put another way: initialData on useQuery also does nothing if you get data into the cache before calling it by any other means, like by calling prefetchQuery or by calling queryClient.setQueryData. So this is on purpose. the thing that is different for hydration from the other ways is that hydration really sets that data as initial state, while all the other methods create an empty query first (with initial data undefined) and then just regularly perform a setData call. The difference is small and only visible when you call resetQueries, because with prefetching or setQueryData prefilling, reset will revert back to empty state + loading, while if you initialized with hydration, it will revert back to that state.
foreign-sapphire
foreign-sapphireOP•2y ago
Yes indeed. So for resetQueries the behavior changes and becomes unpredictable as soon as you move from QueryClientProvider to PersistQueryClientProvider
solid-orange
solid-orange•2y ago
It's merely a question of what we define as "initial". restoring something from an external storage could very well be seen as this data being seen as initial from when the app loaded
foreign-sapphire
foreign-sapphireOP•2y ago
Yeah you're right. But wouldn't you expect that if you set initialData in your useQuery that if you reset, that initial data will be loaded. Even after a persisted state was loaded? Just looking at it again, but none of the options is saved to persistent storage. networkMode, retryDelay , meta, etc... Will all take an default value.
solid-orange
solid-orange•2y ago
yes, that's on purpose. we only store data, not options
foreign-sapphire
foreign-sapphireOP•2y ago
That means that if I define a query using an option it would also not survive a restart. In that way initialData is also an option. Maybe adding a persistOptions option that defaults to false to createSyncStoragePersister would solve all these things? Or just only this?
continuing-cyan
continuing-cyan•2y ago
I cant remember why I did that unfortunately, so I wouldnt trust it was for a good reason. 😅
solid-orange
solid-orange•2y ago
k, I might remove it as a bug fix. Need to think about it. Right now hydration is the only way to get a query with initialState so that it resets to that. Might also go the other way and have setQueryData also do that. Not sure yet 🙂
foreign-sapphire
foreign-sapphireOP•2y ago
I think for me having the option to persist all options of a query (thus including initialData) would be the best solution. That way you can make sure Queries will behave exactly the same after a restart. In the current situation it will loose all options (except the ones set by defaultOptions), and thus the behavior of the query might change completely after a restart.
solid-orange
solid-orange•2y ago
We can't persist options because some are observer level options, and they can also be functions which are not persistable
foreign-sapphire
foreign-sapphireOP•2y ago
Hmm yes that is true In that case, you could never recreate the same state after a restart using the current way state is hydrated. Another solution would be to only hydrate the state after a query was created by useQuery. So as soon as a query in initialized for the first time it will check the persisted cach if a state exists and load that into state. This way you can have the options and initial data set first before hydrating the state.
solid-orange
solid-orange•2y ago
if you want that, have a look at the v5 create_persister. It pretty much does that
foreign-sapphire
foreign-sapphireOP•2y ago
Oh nice. I didnt know there was a different way to persist. I'll have a look at it
foreign-sapphire
foreign-sapphireOP•2y ago
You are referring to experimental_createPersister? https://tanstack.com/query/latest/docs/react/plugins/createPersister
experimental_createPersister | TanStack Query Docs
Installation This utility comes as a separate package and is available under the '@tanstack/query-persist-client-core' import.
solid-orange
solid-orange•2y ago
Yes
foreign-sapphire
foreign-sapphireOP•2y ago
I tried it out, but in that case the resetQueries is not working at all 😢 I'll have to debug it to see why it does not do that See my forked sandbox using experimental_createPersister https://codesandbox.io/p/sandbox/query-async-test-forked-kzdfdt Looks like it is reset, becomes stale, but a refetch then immediately retrieves data from persisted storage instead of running the function query.
solid-orange
solid-orange•2y ago
Because it will restore every time when it runs the queryFn and there's no data in it. I don't quite understand why you want to restore initially but then want to/ expect a hard loading state ?
foreign-sapphire
foreign-sapphireOP•2y ago
Ive got a list of items. On app start I want to show cached data, but as soon as the list is refreshed from the server, I need to invalidate/remove to data for the items from the cache to make sure I dont show any old data. I prefer completely removing them from the cache/resetting them to an empty initialState to do that @TkDodo 🔮 I've been looking at experimental_createPersister again to see if this will work for my project. I am now also having issues where I want to have a different staleTime and gcTime on queries. Since the options are not persisted when using PersistQueryClientProvider this will not work across app restarts. They will be reset to default values on hydrate. One thing I noticed when looking at how experimental_createPersister is implemented is that it does not garbage collect any data from storage when that data is not accessed anymore. This could cause a lot of stale items in storage that will never be garbage collected. I am right here or did I miss something and is there a garbage collector implemented somewhere?

Did you find this page helpful?