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

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>
  )
}

问题原因

  1. 返回对象的引用频繁变化:useFetchAccounts每次执行时,都会创建全新的accounts和accountIds对象返回。React的依赖项对比是基于引用相等性的,哪怕对象内部的数组/Set内容完全没变,只要引用变了,组件中依赖这两个对象的useEffect就会触发。
  2. 链式状态更新形成循环:组件内的useEffect触发后,会更新accountsC和accountIdsC,进而触发依赖这两个状态的另一个useEffect更新pageC,组件因此重渲染。重渲染又会重新调用useFetchAccounts,生成新的引用对象,再次触发useEffect,形成无限循环。
  3. 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.17 15:40:21