react-native icon indicating copy to clipboard operation
react-native copied to clipboard

Refresh control with react-query is jumping

Open AleksandrNikolaevich opened this issue 3 years ago • 5 comments

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

AleksandrNikolaevich avatar Jan 06 '22 10:01 AleksandrNikolaevich

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>
  );
};

Sourabh2001-dec avatar Jan 09 '22 12:01 Sourabh2001-dec

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

SEBIIWA avatar Jun 15 '22 22:06 SEBIIWA

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>
);

mozzius avatar Apr 24 '23 13:04 mozzius

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)

DinoWithCurls avatar Feb 01 '24 07:02 DinoWithCurls

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} />

bryanprimus avatar May 06 '24 05:05 bryanprimus