为何基于SWR的自定义React Hook会引发无限循环?
问题描述
我实现了一个自定义React Hook用于从API获取组织数据,并且需要通过URL参数里的organizationId补充返回数据中缺失的id字段。代码如下:
export const useOrganization = () => { const [data, setData] = useState<OrganizationModel | undefined>(); const { organizationId } = useParams(); const { data: dataSWR } = useSWRImmutable< AxiosResponse<Omit<OrganizationModel, 'id'>> >(`organizations/${organizationId}`, api); useEffect(() => { if (dataSWR?.data && organizationId) { setData({ id: organizationId, ...dataSWR.data }); console.log({ id: organizationId, ...dataSWR.data }); } }); return data; };
奇怪的是,当我直接用setData(dataSWR.data)时程序运行正常,但一旦合并id字段写成setData({ id: organizationId, ...dataSWR.data }),Hook就会触发无限循环,想知道这是为什么?
原因分析
你遇到的无限循环问题核心在于**useEffect缺少依赖数组**,并且每次合并生成的都是一个全新的对象引用,导致状态更新后组件重新渲染,进而再次触发useEffect,形成闭环。
具体拆解一下:
- 无依赖的
useEffect会每次渲染都执行:你的useEffect没有指定第二个参数(依赖数组),这意味着它会在组件的每一次渲染完成后都运行一遍。 - 合并操作生成新对象引用:当你执行
{ id: organizationId, ...dataSWR.data }时,每次都会创建一个全新的JavaScript对象。即使对象里的属性值完全一样,它的内存引用地址也是不同的。 - 状态更新触发重新渲染:调用
setData传入新对象后,data状态发生变化,组件(以及Hook所在的上下文)会重新渲染。 - 循环闭环形成:重新渲染后,
useEffect又会再次执行,检查到dataSWR?.data和organizationId存在,就再次创建新对象并调用setData,如此反复循环。
而当你直接用setData(dataSWR.data)时,dataSWR.data是SWR缓存的同一个对象引用(只要API返回的数据没变化),所以调用setData时,React会对比新旧状态的引用,发现引用相同就不会触发组件重新渲染,自然不会导致循环。
解决方法
有两种简单的方式可以解决这个问题:
方法一:给useEffect添加正确的依赖数组
确保useEffect只在dataSWR.data或organizationId发生变化时才执行:
export const useOrganization = () => { const [data, setData] = useState<OrganizationModel | undefined>(); const { organizationId } = useParams(); const { data: dataSWR } = useSWRImmutable< AxiosResponse<Omit<OrganizationModel, 'id'>> >(`organizations/${organizationId}`, api); useEffect(() => { if (dataSWR?.data && organizationId) { setData({ id: organizationId, ...dataSWR.data }); } }, [dataSWR?.data, organizationId]); // 添加依赖数组 return data; };
这样只有当dataSWR.data或者organizationId的引用/值发生变化时,useEffect才会执行,避免了不必要的重复触发。
方法二:直接在返回时合并id,无需额外状态
其实你完全可以省去useState和useEffect的环节,直接在Hook返回的时候合并id字段,这样更简洁高效:
export const useOrganization = () => { const { organizationId } = useParams(); const { data: dataSWR } = useSWRImmutable< AxiosResponse<Omit<OrganizationModel, 'id'>> >(`organizations/${organizationId}`, api); if (!dataSWR?.data || !organizationId) return undefined; return { id: organizationId, ...dataSWR.data } as OrganizationModel; };
这种方式不需要维护额外的本地状态,直接基于SWR返回的数据进行转换,既避免了循环问题,也简化了代码逻辑。
内容的提问来源于stack exchange,提问作者Adam Zálešák

