You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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无关。

你忽略的关键点

  1. select的执行逻辑:React-Query每次从缓存或网络读取原始数据后,都会执行select做数据转换——哪怕原始数据和之前完全一致,只要组件重新渲染触发useQuery执行,select就会跑一遍。staleTime: Infinity只是让数据永远不标记为过时,不会跳过select的转换步骤。
  2. 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.

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.09 16:24:57