Challenges of using React-query V3 with React native:

Nawfal Haddi
5 min readJan 11, 2022

Using react-query in your react-native project will help you gain an incredible performance. Meanwhile, it will allow you to manage server data in your app easily.

React query works perfectly on the web, and it has over 1 million downloads per week.

But when it comes to using it in react native, you should know that there is no light without darkness. In other words, there are some different behaviors that we need to handle and be aware of.

I will list in this article the pain points that I had during working with react-query in my react native project and how I solved them.

1- Make sure the data is up to date on the active screen:

The navigation in mobile is a bit different than the web, as we can read from react-navigation docs:

If you are coming to react-navigation from a web background, you may assume that when the user navigates from route A to route B, A will unmount (its componentWillUnmount is called) and A will mount again when a user comes back to it. While these React lifecycle methods are still valid and are used in react-navigation, their usage differs from the web. This is driven by the more complex needs of mobile navigation.

Example scenario​
Consider a native stack navigator with screens A and B. After navigating to A, its componentDidMount is called. When pushing B, its componentDidMount is also called, but A remains mounted on the stack and its componentWillUnmount is therefore not called.
When going back from B to A, componentWillUnmount of B is called, but componentDidMount of A is not because A remained mounted the whole time.

Based on the react-navigation life cycle, you will find out that mobile navigation is slightly different from web navigation. So for this specific case, react-query doesn’t know that your screen has been focused again.

That’s why to keep the correct behavior on mobile. You need to add the following code to every use of useQuery in your app:

import { useIsFocused } from '@react-navigation/native';
const isFocused = useIsFocused();
const {
data,
isError,
isLoading,
Refetch,
} = useQuery('todos',{ enabled: isFocused });

In this way, the useQuery will refetch data whenever your screen is focused.

2- Keep the lists up to date:

In your react-native app, whether you are using Flatlist or mapping an array inside a ScrollView, you need to make sure that your data is updated and your list re-render whenever your data is changed.

But actually, what happens is whenever you change the data in the cache, it won’t trigger a re-rendering unless you leave the screen and go back to it, which gives a bad experience for users.

For this case, I found a pretty simple solution: telling react-query to notify the component whenever the data is changed.

We can do that by using the option notifyOnChangeProps, and this option allows us to configure when a component should re-render on a granular level.

Also, adding isFetched prop to extraData prop of Flatlist tells the list to re-render whenever isFetched changes, which is precisely what we need when our data is done fetching.

Here is how I did it:

const { isLoading, data, isFetching, refetch, isFetched } =
useQuery("addresses",{
enabled: isFocused,
notifyOnChangeProps: [
'data',
'isFetched',
'isFetching',
'isLoading',
],
});

<FlatList
contentContainerStyle={styles.contentContainerStyle}
onRefresh={() => {
refetch();
}}
refreshing={isFetching}
extraData={isFetched}
data={ordersList}
keyExtractor={(item, index) => index.toString()}
renderItem={renderItem}
ListFooterComponent={renderListFooter}
showsVerticalScrollIndicator={false}
/>

3- Using useInfiniteQuery in react-native:

Implementing infinite scroll in react-native is tricky as we should be cautious about the performance.

On the other hand, when we use useInfiniteQuery, it returns pages where each page has its data, and we can not use pages as data.

Mapping all pages and displaying all the data doesn’t cause any problem on the web. But when it comes to react-native, we have to use FlatList for sure, and there is no escape from that.

So, in this case, we can not pass directly the data returned from useInifniteQuery to the Flatlist. We have to restructure it first and then pass it to the data prop in Flatlist.

Without further talking, I will show you how I did it:

useInfiniteQuery API call:

const { 
isLoading,
Data,
isFetching,
fetchNextPage,
refetch,
isFetched
}= useInfiniteQuery(
'orders-list',
({ pageParam = {currentPage: 1} }) =>
fetchOrders(pageParam)
.then(response => response?.data)
.catch(error => error) ,
{
getNextPageParam: nextPage => {
if (nextPage.currentPage >= nextPage.totalPages) {
return undefined;
} else {
return {currentPage: nextPage.currentPage + 1};

}
},
{
enabled: isFocused,
notifyOnChangeProps: [
'data',
'isFetched',
'isFetching',
'isLoading',
],
},
},
);

Restructuring the Array of data:

const ordersList = useMemo(() => data?.pages?.flatMap((page) => page?.data?.order)?.filter(Boolean), [data]);

Rendering functions:

const renderItem = useCallback(
({ item, index }) => <OrderItem order={item} key={index.toString()} />,
[],
);
const renderListFooter = useCallback(
() =>
isFetching ? (
<Loader size="large" color={“blue”} />
) : null,
[isFetching],
);

Flatlist :

<FlatList
contentContainerStyle={styles.contentContainerStyle}
onRefresh={() => {
refetch();
}}
refreshing={isFetching}
extraData={isFetched}
data={ordersList}
keyExtractor={(item, index) => index.toString()}
renderItem={renderItem}
ListFooterComponent={renderListFooter}
onEndReached={() => {
fetchNextPage();
}}
showsVerticalScrollIndicator={false}
/>

For more information, you can look to useInfiniteQuery documentation.

4- Update a specific component when the cache is changed:

In one of the use cases I had, I used the useQuery in the child component, and I needed to update the parent component when the cache was changed.

I tried first using queryData like this :

queryClient.getQueryData(‘todos’)

but it doesn’t trigger an update when the cache data is changed, and also, it doesn’t have an option to enable this feature.

The second solution that worked for me was I used queryCache.subscribe(callback). This method runs the callback function whenever the cache is updated.

Here is the implementation:

const callback = useCallback(
event => {
if (event?.query?.queryKey === 'todos') {
update();
}
},
[],
);
//subscribe to cache changes
useEffect(() => {
const unsubscribe = queryCache.subscribe(callback);
return () => {
//unsubscribe to cache changes when unmounting the component
unsubscribe();
};
}, [callback, queryCache]);

5- Refetch on app focus & Online status management:

To add these two behaviors to your react native app, you will find straightforward explanations in the react-query official docs.

Here is the link: https://react-query.tanstack.com/react-native.

Finally, I would recommend anyone to use react-query for their react-native projects as it introduced a fantastic performance improvement for our mobile app.

I want to thank my teammates in Tradeling.com, who helped me solve all these problems and deliver a good-performing app.

--

--

Nawfal Haddi

React and React native enthusiast | Front-end engineer @Tradeling.com