react-native
react-native copied to clipboard
Refresh control with react-query is jumping
Description
Content inside ScrollView
is jumping when I use react-query (useQuery hook) with RefreshControl
.
https://user-images.githubusercontent.com/29212209/148366944-fffc035d-718e-4425-9667-0b37ad66737d.mov
Version
0.66.4
Output of npx react-native info
System:
OS: macOS 12.1
CPU: (8) x64 Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz
Memory: 1.95 GB / 16.00 GB
Shell: 3.2.57 - /bin/bash
Binaries:
Node: 15.12.0 - /usr/local/bin/node
Yarn: 1.22.17 - /usr/local/bin/yarn
npm: 7.6.3 - /usr/local/bin/npm
Watchman: 2021.11.08.00 - /usr/local/bin/watchman
Managers:
CocoaPods: 1.11.2 - /usr/local/bin/pod
SDKs:
iOS SDK:
Platforms: DriverKit 21.2, iOS 15.2, macOS 12.1, tvOS 15.2, watchOS 8.3
Android SDK:
API Levels: 23, 24, 25, 26, 27, 28, 29, 30, 31
Build Tools: 23.0.1, 23.0.2, 23.0.3, 25.0.0, 25.0.1, 25.0.2, 25.0.3, 26.0.2, 26.0.3, 27.0.1, 27.0.3, 28.0.2, 28.0.3, 29.0.2, 29.0.3, 30.0.2, 30.0.3
Android NDK: Not Found
IDEs:
Android Studio: 2020.3 AI-203.7717.56.2031.7935034
Xcode: 13.2.1/13C100 - /usr/bin/xcodebuild
Languages:
Java: 1.8.0_181 - /usr/bin/javac
npmPackages:
@react-native-community/cli: Not Found
react: 17.0.2 => 17.0.2
react-native: 0.66.4 => 0.66.4
react-native-macos: Not Found
npmGlobalPackages:
*react-native*: Not Found
Steps to reproduce
pull to refresh on real iOS device
Snack, code example, screenshot, or link to a repository
clone repo https://github.com/AleksandrNikolaevich/refresh-control-problem
or
App.js
import React from 'react';
import {
RefreshControl,
ScrollView,
StyleSheet,
View,
SafeAreaView,
} from 'react-native';
import {QueryClient, QueryClientProvider, useQuery} from 'react-query';
const queryClient = new QueryClient();
const request = () => fetch('https://google.com', {method: 'GET'});
const Example = () => {
const {isRefetching, refetch} = useQuery('example', request);
return (
<SafeAreaView style={styles.root}>
<ScrollView
refreshControl={
<RefreshControl onRefresh={refetch} refreshing={isRefetching} />
}>
<View style={styles.block}>
<View style={styles.content} />
</View>
</ScrollView>
</SafeAreaView>
);
};
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<Example />
</QueryClientProvider>
);
}
const styles = StyleSheet.create({
root: {
flex: 1,
},
block: {
height: 2000,
},
content: {
height: 200,
width: 200,
backgroundColor: 'red',
},
});
package.json
{
"name": "refreshing",
"version": "0.0.1",
"private": true,
"scripts": {
"android": "react-native run-android",
"ios": "react-native run-ios",
"start": "react-native start",
"test": "jest",
"lint": "eslint ."
},
"dependencies": {
"react": "17.0.2",
"react-native": "0.66.4",
"react-query": "^3.34.7"
},
"devDependencies": {
"@babel/core": "^7.12.9",
"@babel/runtime": "^7.12.5",
"@react-native-community/eslint-config": "^2.0.0",
"babel-jest": "^26.6.3",
"eslint": "7.14.0",
"jest": "^26.6.3",
"metro-react-native-babel-preset": "^0.66.2",
"react-test-renderer": "17.0.2"
},
"jest": {
"preset": "react-native"
}
}
P.S. linked problem https://github.com/tannerlinsley/react-query/issues/2380
I also came across with this issue but managed to solve with the following workaround:
// created a hook for this purpose and use a self managed state for loading
import React from 'react';
const useRefreshing = refetch => {
const [isRefreshing, setIsRefreshing] = React.useState(false);
const refresh = React.useCallback(async () => {
try {
setIsRefreshing(true);
await refetch();
} catch (error) {
console.log(error);
} finally {
setIsRefreshing(false);
}
}, [refetch]);
return [isRefreshing, refresh];
};
export default useRefreshing;
Now use above hook for refresh purpose:
const Example = () => {
const {isRefetching, refetch} = useQuery('example', request);
const [isRefreshing, refresh] = useRefreshing(refetch)
return (
<SafeAreaView style={styles.root}>
<ScrollView
refreshControl={
<RefreshControl onRefresh={refresh} refreshing={isRefreshing} />
}>
<View style={styles.block}>
<View style={styles.content} />
</View>
</ScrollView>
</SafeAreaView>
);
};
I came across this problem and I think the solution above me is might work because I did not test it myself, but i found this with not jumping or problem and here is an example
let fetchWeather = function () {
return fetch(
`http://api.weatherapi.com/v1/current.json?key=${myapikey}&q=${query}&aqi=no`
).then((res) => {
return res.json()
})
}
const { data, refetch } = useQuery('@weather', fetchWeather)
const [refresh, setRefresh] = useState(false)
we can use the refetch function provided in the returned object in useQuery within a useCallBack hook and since refetch returns a promise we can do it like this and thanks to this you can track refresh state easily without jumping
let onRefresh = useCallback(() => {
setRefresh(true)
refetch().then(() => setRefresh(false))
}, [])
then finally
<AreaView
refreshControl={
<RefreshControl refreshing={refresh} onRefresh={onRefresh} />
}>
{...{something}}
</AreaView>
be aware the AreaView is a component made with styled component for ScrollView and it uses the same props that ScrollView provides
I abstracted this into a hook:
export function useUserRefresh<T>(refetch: () => Promise<T>) {
const [refreshing, setRefreshing] = useState(false);
const handleRefresh = useCallback(() => {
setRefreshing(true);
refetch().then(() => setRefreshing(false));
}, []);
return { refreshing, handleRefresh };
}
You can then use it like this:
const query = useQuery(/* */);
const { refreshing, handleRefresh } = useUserRefresh(query.refetch);
return (
<AreaView refreshControl={<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} />}>
{/* do something with data */}
</AreaView>
);
I also came across with this issue but managed to solve with the following workaround:
// created a hook for this purpose and use a self managed state for loading import React from 'react'; const useRefreshing = refetch => { const [isRefreshing, setIsRefreshing] = React.useState(false); const refresh = React.useCallback(async () => { try { setIsRefreshing(true); await refetch(); } catch (error) { console.log(error); } finally { setIsRefreshing(false); } }, [refetch]); return [isRefreshing, refresh]; }; export default useRefreshing;
Now use above hook for refresh purpose:
const Example = () => { const {isRefetching, refetch} = useQuery('example', request); const [isRefreshing, refresh] = useRefreshing(refetch) return ( <SafeAreaView style={styles.root}> <ScrollView refreshControl={ <RefreshControl onRefresh={refresh} refreshing={isRefreshing} /> }> <View style={styles.block}> <View style={styles.content} /> </View> </ScrollView> </SafeAreaView> ); };
This still works beautifully, but with one fix. Instead of
return [isRefreshing, refresh]
and
const [isRefreshing, refresh] = useRefreshing(refetch)
,
I had to use
return {isRefreshing, refresh}
and const {isRefreshing, refresh} = useRefreshing(refetch)
Custom Hook
// useQueryRefresh.ts
export const useQueryRefresh = <T>(promiseFunctions: (() => Promise<T>)[]) => {
const [refreshing, setRefreshing] = useState(false);
const handleRefresh = useCallback(async () => {
setRefreshing(true);
await Promise.all(promiseFunctions.map((promiseFunction) => promiseFunction()));
setRefreshing(false);
}, [promiseFunctions]);
return { refreshing, handleRefresh };
};
Usage
const { refreshing, handleRefresh } = useQueryRefresh([
() => queryClient.invalidateQueries({ queryKey: ['getUser'] }),
() => queryClient.invalidateQueries({ queryKey: ['getAccount'] }),
]);
<FlatList refreshing={refreshing} onRefresh={handleRefresh} />