reduxjs / redux-toolkit

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

RTK Query stuck at isFetching always "true" #4496

Closed atalayio closed 3 months ago

atalayio commented 3 months ago

I check isFetching with RTk Query and add the loading component accordingly, but sometimes when I experience internet problems (such as the internet suddenly cutting off), isFetching remains true forever. Even if I close and reopen the application, it doesn't work. How can I go about this? Here is the code:

import { View, Text, Pressable } from "react-native"
import { useGetApiKnittingHallMasterHallsQuery } from "../../api/generatedAlpinApi"
import { useEffect, useState } from "react";
import { useNotify } from "../../hooks";
import { Button, HallData, PlantData } from "../../components/ui";
import { useAppSelector } from "../../store";
import LottieView from "lottie-react-native";
import classNames from "classnames";

const Productivity = () => {
    const notify = useNotify();

    const { user, auth } = useAppSelector((x) => x.user)

    const [hallDatas, setHallDatas] = useState([]);

    const getHalls = useGetApiKnittingHallMasterHallsQuery(undefined, { pollingInterval: 60000, refetchOnMountOrArgChange: true });

    useEffect(() => {
        if (!getHalls.data || !getHalls.data.data) {
            // notify({
            //     title: "Hata!",
            //     description: "Bağlantı hatası.",
            //     color: "error"
            // })
            setHallDatas([])
            return;
        }
        setHallDatas(getHalls.data.data)
    }, [getHalls.data])

    return (
        <View className="flex flex-1 h-full bg-[#28313E] dark: dark:bg-neutral-800" >
            {getHalls.isFetching ? (
                <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
                    <LottieView
                        style={{ width: 300, height: 300 }}
                        source={require("../../assets/animations/loading.json")}
                        autoPlay
                        loop
                    />
                </View>
            ) : (
                <View className={classNames("flex flex-1  flex-row bg-[#28313E] mb-10 dark: dark:bg-neutral-800",
                    { "mt-20": getHalls?.data?.data.length === 6 || getHalls?.data?.data.length < 6 }
                )}>
                    <View className={classNames("flex-2 flex-col justify-center items-center", {
                        "mt-14": getHalls?.data?.data.length === 8
                    })}>
                        {hallDatas.length > 0 && (
                            <View className="flex-1 justify-center items-center">
                                <PlantData data={hallDatas[0]} />
                                <Button
                                    isBlock
                                    onPress={() => {
                                        getHalls.refetch()
                                    }}
                                    label="Tazele"
                                    color="primary"
                                    disabled={getHalls.isFetching}
                                />
                            </View>
                        )}
                    </View>
                    <View className="h-full" >
                        <View style={{ marginRight: 310 }} className="flex-4 flex-row flex-wrap justify-center">
                            {hallDatas.map((value, index) => {
                                return <HallData key={index} data={value} />;
                            })}
                        </View>
                    </View>
                </View>
            )
            }
        </View >
    )
}

export { Productivity } 
markerikson commented 3 months ago

I'm confused. If you close and reopen the app, you should be getting a brand new Redux store and JS environment. Are you persisting the Redux state and RTK Query data somehow?

atalayio commented 3 months ago

I'm confused. If you close and reopen the app, you should be getting a brand new Redux store and JS environment. Are you persisting the Redux state and RTK Query data somehow?

Nope, I don't do any cache operation, but isFetching true continues to come even when I close and open the application. I don't know how and I can't prevent it.

atalayio commented 3 months ago

I'm confused. If you close and reopen the app, you should be getting a brand new Redux store and JS environment. Are you persisting the Redux state and RTK Query data somehow?

Please check the video:

https://github.com/reduxjs/redux-toolkit/assets/106342678/02ad053e-1a04-4df0-b57e-fe7dd3a314a5

phryneas commented 3 months ago

Please show your store setup. It's very likely that you use something like redux-persist, which would need extra setup for RTK Query.

atalayio commented 3 months ago

Please show your store setup. It's very likely that you use something like redux-persist, which would need extra setup for RTK Query.

This is my _layout.tsx;

import { useEffect } from "react";

import * as SplashScreen from "expo-splash-screen";
import { ErrorBoundary, router, Slot } from "expo-router";
import { StatusBar } from "expo-status-bar";
import { useFonts } from "expo-font";
import FontAwesome from "@expo/vector-icons/FontAwesome";

import { Provider } from "react-redux";
import { PersistGate } from "redux-persist/es/integration/react";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import {
  initialWindowMetrics,
  SafeAreaProvider,
} from "react-native-safe-area-context";

import reduxStore, {
  useAppSelector,
} from "../store";
import {
  AlertContextProvider,
  ThemeContextProvider,
  useAlertContext,
  HeaderRightContextProvider,
  NotifyContextProvider,
} from "../contexts";
import { Alert } from "../components/alert";
import { Notify } from "../components/notify";

SplashScreen.preventAutoHideAsync();

const RootLayout = () => {
  const { store, persistor } = reduxStore();

  const [loaded, error] = useFonts({
    SpaceMono: require("../assets/fonts/SpaceMono-Regular.ttf"),
    ...FontAwesome.font,
  });

  useEffect(() => {
    if (error) throw error;
  }, [error]);

  if (!loaded) return null;

  return (
    <GestureHandlerRootView style={{ flex: 1 }}>
      <Provider store={store}>
        <PersistGate loading={null} persistor={persistor}>
          <MiddlewareLayout />
        </PersistGate>
      </Provider>
    </GestureHandlerRootView>
  );
};

const MiddlewareLayout = () => {

  return (
    <NotifyContextProvider>
      <AlertContextProvider>
        <HeaderRightContextProvider>
          <InitialLayout />
        </HeaderRightContextProvider>
      </AlertContextProvider>
    </NotifyContextProvider>
  );
};

const InitialLayout = () => {
  const { theme } = useAppSelector((x) => x.app);
  const { auth } = useAppSelector((x) => x.user);
  const {
    open: isAlertOpen,
    options: alertOptions,
    onClose: onCloseAlert,
  } = useAlertContext();
  // const { updateProgress } = useUpdate();

  useEffect(() => {
    if (!auth) router.replace("/login");

    SplashScreen.hideAsync();
  }, []);

  return (
    <ThemeContextProvider>
      <SafeAreaProvider initialMetrics={initialWindowMetrics}>
        <Alert
          isOpen={isAlertOpen}
          options={alertOptions}
          onClose={onCloseAlert}
        />
        <Notify />
        <Slot />
        <StatusBar style={theme === "dark" ? "light" : "dark"} />
      </SafeAreaProvider>
    </ThemeContextProvider>
  );
};

export { ErrorBoundary };
export default RootLayout;

and this is my store/index.ts

import { combineReducers, configureStore } from "@reduxjs/toolkit";
import { useDispatch, useSelector, TypedUseSelectorHook } from "react-redux";
import { persistStore, persistReducer } from "redux-persist";

import AsyncStorage from "@react-native-async-storage/async-storage";

import app, { changeApi, themeChange, useDeviceThemeChange } from "./app";
import user, { userLogin, userLogout } from "./user";
import { api } from "../api/api";

const rootReducer = combineReducers({
  app,
  user,
  [api.reducerPath]: api.reducer,
});

const persistConfig = {
  key: "root",
  storage: AsyncStorage,
};

const persistedReducer = persistReducer(persistConfig, rootReducer);

export type RootState = ReturnType<typeof rootReducer>;

const store = configureStore({
  reducer: persistedReducer,
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware({
      serializableCheck: false,
      immutableCheck: false,
    }).concat(api.middleware),
});

export type AppDispatch = typeof store.dispatch;

export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

export { changeApi, themeChange, useDeviceThemeChange, userLogin, userLogout };

export default () => {
  let persistor = persistStore(store);

  return { store, persistor };
};
phryneas commented 3 months ago

grafik Why do you say that you are not using persistance? Pretty much every second word there is persist.

You need to apply the steps described at this documentation page. If you don't, you get the behaviour you are currently seeing. https://redux-toolkit.js.org/rtk-query/usage/persistence-and-rehydration#redux-persist

atalayio commented 3 months ago

grafik Why do you say that you are not using persistance? Pretty much every second word there is persist.

You need to apply the steps described at this documentation page. If you don't, you get the behaviour you are currently seeing. https://redux-toolkit.js.org/rtk-query/usage/persistence-and-rehydration#redux-persist

Hello, I tried your solution for now and getting this; code:

import isomorphicFetch from "isomorphic-fetch";
import { Mutex } from "async-mutex";

import {
  BaseQueryFn,
  createApi,
  FetchArgs,
  fetchBaseQuery,
  FetchBaseQueryError,
} from "@reduxjs/toolkit/query/react";

import { RootState } from "../store";
import { Action } from "@reduxjs/toolkit";
import { REHYDRATE } from "redux-persist";

function isHydrateAction(action: Action): action is Action<typeof REHYDRATE> & {
  key: string
  payload: RootState
  err: unknown
} {
  return action.type === REHYDRATE
}

export const baseQuery = (baseUrl: string) =>
  fetchBaseQuery({
    baseUrl,
    fetchFn: isomorphicFetch,
    prepareHeaders: (headers, { getState }) => {
      const state = getState() as RootState;
      const token = (getState() as RootState).user.user?.token;

      headers.set("Content-Type", "application/json");
      if (token) {
        headers.set("Authorization", `Bearer ${token}`);
      }
      return headers;
    },
  });

const mutex = new Mutex();

export const baseQueryWithReauth: BaseQueryFn<
  string | FetchArgs,
  unknown,
  FetchBaseQueryError
> = async (args, api, extraOptions) => {
  const apiUrl = (api.getState() as RootState).app.api;

  if (!apiUrl) {
    return {
      error: {
        status: 400,
        statusText: "Bad Request",
        data: "No Url found",
      },
    };
  }

  await mutex.waitForUnlock();

  let result = await baseQuery(apiUrl)(args, api, extraOptions);

  if (result.error) {
    if (!mutex.isLocked()) {
      const release = await mutex.acquire();

      try {
        result = await baseQuery(apiUrl)(args, api, extraOptions);
      } finally {
        release();
      }
    }
  }

  return result;
};

export const api = createApi({
  baseQuery: baseQueryWithReauth,
  extractRehydrationInfo(action, { reducerPath }): any {
    if (isHydrateAction(action)) {
      if (action.key === 'key used with redux-persist') {
        return action.payload
      }
      return action.payload[api.reducerPath]
    }
  },
  endpoints: () => ({}),
});

error: TypeError: Cannot convert undefined value to object, js engine: hermes

phryneas commented 3 months ago

What line are you getting that error on? Those two return statements seem suspicious to me - if an action is not exactly the right action, you shouldn't return anything from extractRehydrationInfo.

atalayio commented 3 months ago

What line are you getting that error on? Those two return statements seem suspicious to me - if an action is not exactly the right action, you shouldn't return anything from extractRehydrationInfo.

export const api = createApi({
baseQuery: baseQueryWithReauth,
extractRehydrationInfo(action, { reducerPath }): any {
if (isHydrateAction(action)) {
if (action.key === 'key used with redux-persist') {
return action.payload
}
// return action.payload[api.reducerPath]
}
},
endpoints: () => ({}),
});

As you said, I put the second return in the comment line, now there is no error when I start the application even if I have any internet problems with isFetching. Thank you so much! ❤️