React useCollection自定义Hook重渲染后参数未更新问题
问题成因
核心bug出在useCollection自定义Hook中useRef的错误使用:
- 你最初添加
useRef的目的,是解决「每次组件渲染都会新建查询条件数组,数组引用地址变化导致useEffect无限循环触发」的问题,这个思路方向是对的,但实现逻辑完全写反了:const query = useRef(_query).current这类写法,只会在Hook首次挂载时把初始传入的_query参数存到ref对象中,后续组件重渲染时,哪怕传入的_query已经是更新后的新值,useRef也不会主动更新已存储的current值,最终你拿到的query、query2、query3永远是组件第一次挂载时的初始查询条件。 - 当你提交日期筛选、调用setDateStart/setDateEnd触发组件重渲染时,新的日期范围参数确实传到了Hook里,但Hook内部直接把新值忽略了。useEffect的依赖项全是存了初始值的旧变量,根本不会感知到参数变化,自然不会重新执行Firebase数据查询,也就拿不到筛选后的结果。
修复方案
推荐用第一种方案,改动小、兼容性好,不需要修改所有调用Hook的组件代码。
方案1:修正useRef的更新逻辑(推荐)
核心调整是:不要直接把ref初始值的current取出来当常量用,而是在每次渲染时对比传入的新参数和ref中存储的旧参数,只有参数内容真的发生变化时,才更新ref的存储值,同时加入简单的深度判断,避免数组引用变化导致的无限循环。
修改后的useCollection完整代码:
import { useEffect, useState, useRef } from "react"; import { projectFirestore } from "../firebase/config"; // 简单深度比较,适配当前查询参数都是基本类型、Date对象的场景 const isValueEqual = (a, b) => JSON.stringify(a) === JSON.stringify(b); export const useCollection = ( collection, _query, _query2, _query3, _orderBy ) => { const [documents, setDocuments] = useState(null); const [error, setError] = useState(null); // 初始化ref存储查询参数 const queryRef = useRef(_query); const query2Ref = useRef(_query2); const query3Ref = useRef(_query3); const orderByRef = useRef(_orderBy); // 每次渲染对比新旧参数,内容真的变化时才更新ref存储值 if (!isValueEqual(queryRef.current, _query)) queryRef.current = _query; if (!isValueEqual(query2Ref.current, _query2)) query2Ref.current = _query2; if (!isValueEqual(query3Ref.current, _query3)) query3Ref.current = _query3; if (!isValueEqual(orderByRef.current, _orderBy)) orderByRef.current = _orderBy; useEffect(() => { let ref = projectFirestore.collection(collection); if (queryRef.current) ref = ref.where(...queryRef.current); if (query2Ref.current) ref = ref.where(...query2Ref.current); if (query3Ref.current) ref = ref.where(...query3Ref.current); if (orderByRef.current) ref = ref.orderBy(...orderByRef.current); const unsubscribe = ref.onSnapshot( (snapshot) => { const results = snapshot.docs.map(doc => ({ ...doc.data(), id: doc.id })); setDocuments(results); setError(null); }, (error) => { console.log(error); setError("could not fetch the data"); } ); return () => unsubscribe(); }, [collection, queryRef, query2Ref, query3Ref, orderByRef]); return { documents, error }; };
改完之后不需要修改TimeCard组件的任何调用代码,日期筛选提交后Hook就能拿到最新的查询参数,自动重新拉取对应范围的数据。
方案2:组件侧稳定查询参数引用
如果不想修改Hook的公共逻辑,也可以在TimeCard组件侧用useMemo包裹所有查询条件数组,只有当依赖的状态真的变化时才生成新数组,同时删掉Hook里错误的ref固定值逻辑:
- 修改TimeCard中调用useCollection的部分:
import React, { useState, useEffect, useMemo } from "react"; // ...其他导入代码不变 const TimeCard = () => { // ...原有状态逻辑不变 const { uid } = useAuthContext().user; // 用useMemo稳定查询数组的引用 const baseQuery = useMemo(() => ["uid", "==", uid], [uid]); const startQuery = useMemo(() => ["createdAt", ">=", dateStart], [dateStart]); const endQuery = useMemo(() => ["createdAt", "<=", dateEnd], [dateEnd]); const sortRule = useMemo(() => ["createdAt", "asc"], []); const { documents } = useCollection( "timeinout", baseQuery, startQuery, endQuery, sortRule ); // ...原有其他逻辑不变 }
- 修改useCollection代码,删掉所有useRef相关逻辑,直接使用传入的参数作为useEffect依赖:
export const useCollection = ( collection, query, query2, query3, orderBy ) => { const [documents, setDocuments] = useState(null); const [error, setError] = useState(null); useEffect(() => { let ref = projectFirestore.collection(collection); if (query) ref = ref.where(...query); if (query2) ref = ref.where(...query2); if (query3) ref = ref.where(...query3); if (orderBy) ref = ref.orderBy(...orderBy); const unsubscribe = ref.onSnapshot( (snapshot) => { const results = snapshot.docs.map(doc => ({ ...doc.data(), id: doc.id })); setDocuments(results); setError(null); }, (error) => { console.log(error); setError("could not fetch the data"); } ); return () => unsubscribe(); }, [collection, query, query2, query3, orderBy]); return { documents, error }; };
注意:这个方案要求所有调用useCollection的业务组件,都必须自己保证传入的数组、对象类型参数引用稳定,否则还是会触发useEffect无限循环,维护成本比方案1高。
验证方式
修改完成后提交日期筛选,控制台打印的查询参数会同步成你选择的最新日期,Firebase会自动重新拉取对应时间范围的数据,表格展示内容随筛选条件实时更新。
内容的提问来源于stack exchange,提问作者emdee5566
相关产品推荐
相关产品推荐

