为何useCallback函数会导致useEffect出现无限循环?
问题根源
你遇到的无限循环,核心原因是自定义钩子useContentful每次被组件调用时,返回的getContentfulProjectById函数引用都会刷新——哪怕内部用了useCallback,但由于useContentful作为自定义钩子,每次组件渲染都会重新执行一遍,内部的useCallback生成的函数引用也会跟着变成新的。这就导致queryForProject的useCallback依赖项触发更新,进而触发useEffect执行,组件重新渲染,形成循环。
另外你的useContentful代码里还有个小问题:client变量没有定义,应该是漏了createClient的初始化代码,而且如果client是在钩子内部创建的,每次钩子执行都会生成新的客户端,这也会导致useCallback的实际依赖缺失。
解决方案
方案1:从根源稳定函数引用(推荐)
修改useContentful,把client改成单例,并给所有异步函数加上useCallback且依赖正确:
import { createClient } from "contentful"; import { setContentfulProjects, setContentfulStaff, setSelectedProject, } from "store/actions/contentfulActions"; import { store } from "store/store"; import { useCallback } from "react"; // 把Contentful客户端提到钩子外部,作为单例只初始化一次 const client = createClient({ space: "你的spaceId", accessToken: "你的accessToken", // 其他配置项 }); const useContentful = () => { // 给所有异步函数加上useCallback,依赖为空(因为client是外部单例) const getContentfulProjects = useCallback(async () => { try { const entries = await client.getEntries({ content_type: "projects", select: "fields", order: "fields.name", }); store.dispatch(setContentfulProjects(entries)); } catch (error) { console.log(`Error Fetching Programs:${error}`); } }, []); const getContentfulStaff = useCallback(async () => { try { const entries = await client.getEntries({ content_type: "staff", select: "fields", order: "fields.staffId", }); store.dispatch(setContentfulStaff(entries)); } catch (error) { console.log(`Error Fetching Programs:${error}`); } }, []); const getContentfulProjectById = useCallback(async (projectId) => { try { const entries = await client.getEntries({ content_type: "projects", 'fields.projectId': projectId, }); store.dispatch(setSelectedProject(entries)); } catch (error) { console.log(`Error Fetching Programs:${error}`); } }, []); return { getContentfulProjects, getContentfulStaff, getContentfulProjectById, }; }; export default useContentful;
修改后,useContentful返回的所有函数引用都会保持稳定,不会随组件渲染刷新,你的组件代码不需要改动就能解决循环问题。
方案2:简化组件逻辑,跳过中间useCallback
如果暂时不想修改useContentful,可以把请求逻辑直接放进useEffect,省去额外的queryForProject函数:
const { projectId } = useParams(); const { getContentfulProjectById } = useContentful(); const [isLoading, setIsLoading] = useState(true); useEffect(() => { const fetchProject = async () => { await getContentfulProjectById(projectId); setIsLoading(false); }; fetchProject(); }, [getContentfulProjectById, projectId]);
不过这个方案只是临时规避,只要getContentfulProjectById引用还会变化,就可能再次触发循环,所以还是推荐方案1。
方案3:用useRef缓存函数引用
在组件中用useRef保存最新的请求函数,让useEffect只依赖projectId:
const { projectId } = useParams(); const { getContentfulProjectById } = useContentful(); const [isLoading, setIsLoading] = useState(true); const fetchProjectRef = useRef(); // 每次依赖变化时,更新ref里的函数 useEffect(() => { fetchProjectRef.current = async () => { await getContentfulProjectById(projectId); setIsLoading(false); }; }, [getContentfulProjectById, projectId]); // 只在projectId变化时执行请求 useEffect(() => { fetchProjectRef.current(); }, [projectId]);
这个方案能避免循环,但代码复杂度稍高,适合无法修改useContentful的场景。
内容的提问来源于stack exchange,提问作者CookieMonsta89

