React-query 缓存不会在页面刷新时持续存在

React-query cache doesn't persist on page refresh

我想在 useQuery 请求成功后设置 24 小时缓存。

但是我一刷新页面,缓存就没有了。我看到它是因为每次在我的服务器上命中该路由时我 console.log 都会收到一条消息。

如何防止这种行为并实现真正的缓存?

代码如下:

   import { useQuery } from "react-query";
import { api } from "./config";

const _getUser = async () => {
  try {
const res = api.get("/get-user");
return res;
  } catch (err) {
return err;
  }
};

export const getUser = () => {
  const { data } = useQuery("contact", () => _getUser(), {
cacheTime: 1000 * 60 * 60 * 24,
  });
  return { user: data && data.data };
};


// then in the component:
  const { user } = getUser();

return (
<div >
  hello {user?.name}
</div>

我也试过用staleTime替换cacheTime

如果您重新加载浏览器,缓存就会消失,因为缓存存在于内存中。如果你想要一个持久缓存,你可以试试(实验性的)persistQueryClient 插件:https://react-query.tanstack.com/plugins/persistQueryClient

React 查询现在有一个实验性的功能,可以在 localStorage 上持久化内容。 尽管如此,我更喜欢使用自定义挂钩,以使 useQuery 更健壮并将内容持久保存在 localSTorage 中。 这是我的自定义挂钩:

import { isSameDay } from "date-fns";
import { useEffect, useRef } from "react";
import { useBeforeunload } from "react-beforeunload";
import { useQuery, useQueryClient } from "react-query";

import { store as reduxStore } from "../../redux/store/store";

const LOCAL_STORAGE_CACHE_EXPIRY_TIME = 1000 * 60 * 60 * 23; // 23h
const divider = "---$---";
const defaultOptions = {
    persist: true, // set to false not to cache stuff in localStorage
    useLocation: true, // this will add the current location pathname to the component, to make the query keys more specific. disable if the same component is used on different pages and needs the same data
    persistFor: LOCAL_STORAGE_CACHE_EXPIRY_TIME,
    invalidateAfterMidnight: false, // probably you want this to be true for charts where the dates are visible. will overwrite persistFor, setting expiry time to today midnight
    defaultTo: {},
};

const getLocalStorageCache = (dataId, invalidateAfterMidnight) => {
    const data = localStorage.getItem(dataId);
    if (!data) {
        return;
    }
    try {
        const parsedData = JSON.parse(data);
        const today = new Date();
        const expiryDate = new Date(Number(parsedData.expiryTime));
        const expired =
            today.getTime() - LOCAL_STORAGE_CACHE_EXPIRY_TIME >= expiryDate.getTime() ||
            (invalidateAfterMidnight && !isSameDay(today, expiryDate));

        if (expired || !parsedData?.data) {
            // don't bother removing the item from localStorage, since it will be saved again with the new expiry time and date when the component is unmounted or the user leaves the page
            return;
        }

        return parsedData.data;
    } catch (e) {
        console.log(`unable to parse local storage cache for ${dataId}`);
        return undefined;
    }
};

const saveToLocalStorage = (data, dataId) => {
    try {
        const wrapper = JSON.stringify({
            expiryTime: new Date().getTime() + LOCAL_STORAGE_CACHE_EXPIRY_TIME,
            data,
        });
        localStorage.setItem(dataId, wrapper);
    } catch (e) {
        console.log(
            `Unable to save data in localStorage for ${dataId}. Most probably there is a function in the payload, and JSON.stringify failed`,
            data,
            e
        );
    }
};

const clearOtherCustomersData = globalCustomerId => {
    // if we have data for other customers, delete it
    Object.keys(localStorage).forEach(key => {
        if (!key.includes(`preferences${divider}`)) {
            const customerIdFromCacheKey = key.split(divider)[1];
            if (customerIdFromCacheKey && customerIdFromCacheKey !== String(globalCustomerId)) {
                localStorage.removeItem(key);
            }
        }
    });
};

const customUseQuery = (queryKeys, getData, queryOptions) => {
    const options = { ...defaultOptions, ...queryOptions };
    const store = reduxStore.getState();
    const globalCustomerId = options.withRealCustomerId
        ? store.userDetails?.userDetails?.customerId
        : store.globalCustomerId.id;
    const queryClient = useQueryClient();
    const queryKey = Array.isArray(queryKeys)
        ? [...queryKeys, globalCustomerId]
        : [queryKeys, globalCustomerId];
    if (options.useLocation) {
        if (typeof queryKey[0] === "string") {
            queryKey[0] = `${queryKey[0]}--path--${window.location.pathname}`;
        } else {
            try {
                queryKey[0] = `${JSON.stringify(queryKey[0])}${window.location.pathname}`;
            } catch (e) {
                console.error(
                    "Unable to make query. Make sure you provide a string or array with first item string to useQuery",
                    e,
                );
            }
        }
    }
    const queryId = `${queryKey.slice(0, queryKey.length - 1).join()}${divider}${globalCustomerId}`;

    const placeholderData = useRef(
        options.persist
            ? getLocalStorageCache(queryId, options.invalidateAfterMidnight) ||
                  options.placeholderData
            : options.placeholderData,
    );
    const useCallback = useRef(false);
    const afterInvalidationCallback = useRef(null);
    const showRefetch = useRef(false);
    const onSuccess = freshData => {
        placeholderData.current = undefined;
        showRefetch.current = false;
        if (options.onSuccess) {
            options.onSuccess(freshData);
        }
        if (useCallback.current && afterInvalidationCallback.current) {
            afterInvalidationCallback.current(freshData);
            useCallback.current = false;
            afterInvalidationCallback.current = null;
        }

        if (options.persist) {
            if(globalCustomerId){
                saveToLocalStorage(freshData, queryId);
            }
        }
    };

    const data = useQuery(queryKey, getData, {
        ...options,
        placeholderData: placeholderData.current,
        onSuccess,
    });
    const save = () => {
        if (options.persist && data?.data) {
            saveToLocalStorage(data.data, queryId);
        }
    };

    // if there are other items in localStorage with the same name and a different customerId, delete them
    // to keep the localStorage clear
    useBeforeunload(() => clearOtherCustomersData(globalCustomerId));

    useEffect(() => {
        return save;
    }, []);

    const invalidateQuery = callBack => {
        if (callBack && typeof callBack === "function") {
            useCallback.current = true;
            afterInvalidationCallback.current = callBack;
        } else if (callBack) {
            console.error(
                "Non function provided to invalidateQuery. Make sure you provide a function or a falsy value, such as undefined, null, false or 0",
            );
        }
        showRefetch.current = true;
        queryClient.invalidateQueries(queryKey);
    };

    const updateQuery = callBackOrNewValue => {
        queryClient.setQueryData(queryKey, prev => {
            const updatedData =
                typeof callBackOrNewValue === "function"
                    ? callBackOrNewValue(prev)
                    : callBackOrNewValue;
            return updatedData;
        });
    };

    return {
        ...data,
        queryKey,
        invalidateQuery,
        data: data.data || options.defaultTo,
        updateQuery,
        isFetchingAfterCacheDataWasReturned:
            data.isFetching &&
            !placeholderData.current &&
            !data.isLoading &&
            showRefetch.current === true,
    };
};

export default customUseQuery;

有些东西是我的项目特有的,比如 customerId。 我正在使用 onBeforeUnload 删除不属于当前客户的数据,但该项目特定。

您不需要复制粘贴所有这些内容,但我相信在 useQuery 周围有一个自定义挂钩非常方便,因此您可以增加它的潜力并执行 运行 使用新数据的回调之类的事情在之前的数据失效或返回 invalidateQuery/updateQuery 函数后,所以当你想 invalidate/update 查询时不需要使用 useQueryClient。