React自定义Hook结合useEffect触发无限调用问题排查与修复
自定义Hook引发无限重渲染的问题分析与修复
问题背景
重构账户信息获取逻辑时,使用自定义Hook useFetchAccounts 出现了页面无限重渲染的问题,将Hook内的逻辑直接移到组件中后问题消失。相关代码如下:
自定义Hook代码
// flattenTree 将带children的树形结构转为扁平数组 const flattenTree = <T extends { children: T[] }>(treeObject: T) => { const flattenedTree: T[] = [] flattenedTree.push(treeObject) const queue = [treeObject] while (queue.length !== 0) { const searching = queue.shift() for (const child of searching?.children || []) { flattenedTree.push(child) queue.push(child) } } return flattenedTree } const useFetchAccounts = () => { const [accountsA, setAccountsA] = useState([]) const [accountsB, setAccountsB] = useState([]) const [accountIdsA, setAccountIdsA] = useState(new Set()) const [accountIdsB, setAccountIdsB] = useState(new Set()) const { data: accountTreeA } = useSWR( `${FETCH_ACCOUNT_URL}/${ACCOUNTA_ID}`, fetchAccount ) const { data: accountTreeB } = useSWR( `${FETCH_ACCOUNT_URL}/${ACCOUNTB_ID}`, fetchAccount ) useEffect(() => { if (accountTreeA) { setAccountsA(flattenTree(accountTreeA)) } }, [accountTreeA]) useEffect(() => { if (accountTreeB) { setAccountsB(flattenTree(accountTreeB)) } }, [accountTreeB]) useEffect(() => { setAccountIdsA(new Set(accountsA.map((account) => account.accountId))) }, [accountsA]) useEffect(() => { setAccountIdsB(new Set(accountsB.map((account) => account.accountId))) }, [accountsB]) return { accounts: { accountsA, accountsB, }, accountIds: { accountIdsA, accountIdsB, }, } }
使用Hook的组件代码
const TestPage: React.FC = () => { const [accountsC, setAccountsC] = useState<Account[]>([]) const [accountIdsC, setAccountIdsC] = useState<Set<string>>(new Set()) const [pageC, setPageC] = useState<LongShortPage[]>([]) const { accounts, accountIds } = useFetchAccounts() const { data: pageA } = useSWR( { url: FETCH_BOOK_BY_ACCOUNT_ID, targetAccountIds: [...accountIds.accountIdsA], }, fetchPagesByAccountIds ) useEffect(() => { const { accountsA } = accounts const { accountIdsA, accountIdsB } = accountIds setAccountsC(accountsA.filter((account) => !accountIdsB.has(account.accountId))) setAccountIdsC( new Set([...accountIdsA].filter((accountId) => !accountIdsB.has(accountId))) ) }, [accounts, accountIds]) useEffect(() => { if (pageA) { setPageC(pageA.filter((page) => accountIdsC.has(page.accountId))) } }, [pageA, accountIdsC]) return ( <li> {[...accountIdsC].map((id) => ( <div key={id}>${id}</div> ))} {/* 省略其他内容 */} </li> ) }
问题原因
- 返回对象的引用频繁变化:
useFetchAccounts每次执行时,都会创建全新的accounts和accountIds对象返回。React的依赖项对比是基于引用相等性的,哪怕对象内部的数组/Set内容完全没变,只要引用变了,组件中依赖这两个对象的useEffect就会触发。 - 链式状态更新形成循环:组件内的
useEffect触发后,会更新accountsC和accountIdsC,进而触发依赖这两个状态的另一个useEffect更新pageC,组件因此重渲染。重渲染又会重新调用useFetchAccounts,生成新的引用对象,再次触发useEffect,形成无限循环。 - Set实例的引用问题:
useFetchAccounts中每次更新accountIdsA/accountIdsB时,都会创建新的Set实例。即使Set里的元素完全相同,新实例的引用也和旧实例不一样,这会导致返回的accountIds对象引用变化,进一步加剧循环。
修复方案
方案1:用useMemo缓存Hook的返回值
给useFetchAccounts的返回对象加上useMemo缓存,只有当内部的状态真正变化时,才生成新的返回对象:
const useFetchAccounts = () => { const [accountsA, setAccountsA] = useState([]) const [accountsB, setAccountsB] = useState([]) const [accountIdsA, setAccountIdsA] = useState(new Set()) const [accountIdsB, setAccountIdsB] = useState(new Set()) const { data: accountTreeA } = useSWR( `${FETCH_ACCOUNT_URL}/${ACCOUNTA_ID}`, fetchAccount ) const { data: accountTreeB } = useSWR( `${FETCH_ACCOUNT_URL}/${ACCOUNTB_ID}`, fetchAccount ) useEffect(() => { if (accountTreeA) { setAccountsA(flattenTree(accountTreeA)) } }, [accountTreeA]) useEffect(() => { if (accountTreeB) { setAccountsB(flattenTree(accountTreeB)) } }, [accountTreeB]) useEffect(() => { setAccountIdsA(new Set(accountsA.map((account) => account.accountId))) }, [accountsA]) useEffect(() => { setAccountIdsB(new Set(accountsB.map((account) => account.accountId))) }, [accountsB]) // 缓存返回对象,仅当依赖项变化时更新 const result = useMemo(() => ({ accounts: { accountsA, accountsB, }, accountIds: { accountIdsA, accountIdsB, }, }), [accountsA, accountsB, accountIdsA, accountIdsB]) return result }
方案2:移除冗余状态,直接计算Set
accountIdsA/accountIdsB完全是由accountsA/accountsB推导出来的,没必要用useState存储,直接用useMemo计算即可,避免不必要的状态更新:
const useFetchAccounts = () => { const [accountsA, setAccountsA] = useState([]) const [accountsB, setAccountsB] = useState([]) const { data: accountTreeA } = useSWR( `${FETCH_ACCOUNT_URL}/${ACCOUNTA_ID}`, fetchAccount ) const { data: accountTreeB } = useSWR( `${FETCH_ACCOUNT_URL}/${ACCOUNTB_ID}`, fetchAccount ) useEffect(() => { if (accountTreeA) { setAccountsA(flattenTree(accountTreeA)) } }, [accountTreeA]) useEffect(() => { if (accountTreeB) { setAccountsB(flattenTree(accountTreeB)) } }, [accountTreeB]) // 直接计算并缓存Set实例 const accountIdsA = useMemo(() => new Set(accountsA.map(a => a.accountId)), [accountsA]) const accountIdsB = useMemo(() => new Set(accountsB.map(a => a.accountId)), [accountsB]) // 缓存最终返回对象 const result = useMemo(() => ({ accounts: { accountsA, accountsB }, accountIds: { accountIdsA, accountIdsB } }), [accountsA, accountsB, accountIdsA, accountIdsB]) return result }
方案3:优化组件内的SWR依赖项
组件中使用useSWR时,直接传入对象作为key会导致每次渲染都生成新对象,触发SWR重新请求。可以用useMemo缓存这个key对象:
const TestPage: React.FC = () => { const [accountsC, setAccountsC] = useState<Account[]>([]) const [accountIdsC, setAccountIdsC] = useState<Set<string>>(new Set()) const [pageC, setPageC] = useState<LongShortPage[]>([]) const { accounts, accountIds } = useFetchAccounts() // 缓存SWR的key对象,避免每次渲染生成新引用 const swrKey = useMemo(() => ({ url: FETCH_BOOK_BY_ACCOUNT_ID, targetAccountIds: [...accountIds.accountIdsA], }), [accountIds.accountIdsA]) const { data: pageA } = useSWR( swrKey, fetchPagesByAccountIds ) useEffect(() => { const { accountsA } = accounts const { accountIdsA, accountIdsB } = accountIds setAccountsC(accountsA.filter((account) => !accountIdsB.has(account.accountId))) setAccountIdsC( new Set([...accountIdsA].filter((accountId) => !accountIdsB.has(accountId))) ) }, [accounts, accountIds]) useEffect(() => { if (pageA) { setPageC(pageA.filter((page) => accountIdsC.has(page.accountId))) } }, [pageA, accountIdsC]) return ( <li> {[...accountIdsC].map((id) => ( <div key={id}>${id}</div> ))} {/* 省略其他内容 */} </li> ) }
内容的提问来源于stack exchange,提问作者hyeogeon
相关产品推荐
相关产品推荐

