reduxjs / redux-toolkit

The official, opinionated, batteries-included toolset for efficient Redux development
https://redux-toolkit.js.org
MIT License
10.44k stars 1.12k forks source link

Cannot refetch a query that has not been started yet #4375

Open sanduluca opened 2 weeks ago

sanduluca commented 2 weeks ago

We have some crashes reported in our app with the message "Cannot refetch a query that has not been started yet.". We see the crashed in Firebase Crashlytics. I could not repeat the the crash myself. I searched here the issues with the same problem, but could not find a possible solution . The closest related issue seems to be #2889

Here is the crash stats for Android (we have the same and for iOS) image

Here is the screen code example we have:

Example Screen 1

```tsx import { useState } from "react"; import { ScrollView } from "react-native"; import Barcode from "react-native-barcode-builder"; import { Layout, Header } from "components"; import RefreshControl from "components/core/RefreshControl"; import ErrorDataLoad from "components/ErrorDataLoad"; import { useGetInfoQuery } from "store/info"; export function Screen() { const [isRefreshing, setIsRefreshing] = useState(false); const { data, isLoading, error, refetch } = useGetInfoQuery( undefined, { refetchOnMountOrArgChange: true, } ); const onRefresh = () => { setIsRefreshing(true); refetch().finally(() => setIsRefreshing(false)); }; return (

} > {isLoading ? ( ) : error ? ( ) : data ? ( ) : null} ); } ```

Example Screen 2

```tsx import { useState } from "react"; import { FlatList, View } from "react-native"; import { RatingCell, Layout, Header } from "components"; import RefreshControl from "components/core/RefreshControl"; import { useGetRatingGroupsQuery, useGetRatingQuery } from "store/rating"; import { LoadingOverlay } from "components/LoadingOverlay"; import ErrorDataLoad from "components/ErrorDataLoad"; export function RatingScreen() { const { data: rating, error: ratingError, isLoading: isLoadingRating, refetch: refetchRating, } = useGetRatingQuery(undefined, { refetchOnMountOrArgChange: true }); const { data: ratingGroups, error: ratingGroupError, isLoading: isLoadingRatingGroup, refetch: refetchRatingGroup, } = useGetRatingGroupsQuery(undefined, { refetchOnMountOrArgChange: true }); const [isRefreshing, setIsRefreshing] = useState(false); const onRefresh = () => { setIsRefreshing(true); Promise.all([refetchRating(), refetchRatingGroup()]).finally(() => { setIsRefreshing(false); }); }; const error = ratingError || ratingGroupError; const isLoading = isLoadingRating || isLoadingRatingGroup; return (

{isLoading ? ( ) : error ? ( ) : rating && ratingGroups && ? ( <> } refreshControl={ } /> ) : null} ); } ```

phryneas commented 2 weeks ago

We still don't have a reproduction on this. My take is that you somehow save a reference to the refetch method of the first few renders instead of calling refetch on the latest result - you are likely working with a stale closure somewhere.

sanduluca commented 2 weeks ago

I notice one common thing with #2889. We are using lodash debounce library with refetch. Here is the example of how we use it. We define a runDebouncedRetry function that is the result of lodash debounce outside of react context to keep a stable reference. runDebouncedRetry receives a func as a parrameter that is invoked immediately.

import React from "react";
import { View } from "react-native";
import debounce from "lodash/debounce";
import { Button, Typography } from "./core";

const runDebouncedRetry = debounce((func: () => void) => func(), 1000, {
  leading: true,
});

export default function ErrorDataLoad({
  retry,
  message,
  debounceEnabled = true,
  isRefreshing,
}) {
  return (
    <View style={{ flex: 1 }}>
      <View style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
        <View style={{ marginTop: 30 }}>
          <Typography variant="accent24">Ups. something happend</Typography>
        </View>
        <View style={{ marginTop: 22 }}>
          <Typography variant="regular14" color="n700">
            {message}
          </Typography>
        </View>
      </View>

      <View style={{ paddingHorizontal: 20 }}>
        <Button
          loading={isRefreshing}
          label={"Retry"}
          type="Main"
          onPress={() => {
            if (debounceEnabled) {
              return runDebouncedRetry(retry);
            }
            return retry();
          }}
        />
      </View>
    </View>
  );
}