React-Query中useCallback每次组件渲染都执行的问题排查
React-Query中select函数缓存失效问题排查与解决
问题描述
我开发了一个使用TanStack React-Query的React应用,自定义hook从远程服务器获取对象列表,通过select选项在返回数据给消费组件前做处理。因为数据不常变化,我尝试用useCallback缓存这个select函数,期望它只在数据实际变化时执行,但组件每次渲染时这个函数都会触发,加了staleTime: Infinity也没用。代码示例如下:
// hooks.js export const getData = async (): Promise<DataProps[]> => { return await axios.get("/data").then((res) => { return res.data }) } export const useDataQuery = () => { return useQuery({ queryKey: ["/data"], queryFn: getData, select: useCallback((data: DataProps[]): Map<string, DataProps[]> => { /* 创建以日期(YYYY-mm-dd)为键、DataProps数组为值的Map */ const dataMap = new Map<string, dataProps[]>() data?.forEach((element) => { const dataDate = getDataDate(new Date(element.time)) if (!dataMap.has(dataDate)) { dataMap.set(dataDate, []) } const dataOnDate = dataMap.get(dataDate) dataMap.push(element) // 此处存在代码错误:应为dataOnDate.push(element) }) console.log("new DataMap") return dataMap }, []), }) } // component.js export const ListData = () => { const { data: dataMap } = useDataQuery() ... 组件逻辑 ...
核心问题分析
你的假设不完全准确:useCallback仅负责缓存函数本身的引用,无法控制React-Query何时调用这个select函数。React-Query的select执行时机由自身缓存策略决定,和useCallback无关。
你忽略的关键点
select的执行逻辑:React-Query每次从缓存或网络读取原始数据后,都会执行select做数据转换——哪怕原始数据和之前完全一致,只要组件重新渲染触发useQuery执行,select就会跑一遍。staleTime: Infinity只是让数据永远不标记为过时,不会跳过select的转换步骤。useCallback的实际作用:用useCallback包裹select,只是避免每次调用useDataQuery时创建新的函数引用,防止React-Query因select引用变化重新发起查询(React-Query内部会浅比较select,但用useCallback是规范写法),但这并不能阻止select在每次数据读取时执行。
正确的缓存方式
要让转换后的dataMap仅在原始数据变化时重新生成,推荐将转换逻辑从select中抽离,用useMemo缓存结果:
修正后的代码
export const useDataQuery = () => { // 先获取原始数据,用staleTime让数据永久缓存 const { data: rawData } = useQuery({ queryKey: ["/data"], queryFn: getData, staleTime: Infinity, }) // 用useMemo缓存转换后的Map,仅在rawData变化时重新计算 const dataMap = useMemo(() => { if (!rawData) return new Map() const map = new Map<string, DataProps[]>() rawData.forEach((element) => { const dataDate = getDataDate(new Date(element.time)) if (!map.has(dataDate)) { map.set(dataDate, []) } // 修复原代码的push错误 map.get(dataDate)?.push(element) }) console.log("new DataMap") return map }, [rawData]) return { data: dataMap } }
额外提示
原代码中的dataMap.push(element)是错误写法:Map对象没有push方法,需要先通过get拿到对应日期的数组,再执行push操作。
内容的提问来源于stack exchange,提问作者Chen A.
相关产品推荐
相关产品推荐

