Home > Net >  Custom hooks are called infinitely when used with useEffect
Custom hooks are called infinitely when used with useEffect

Time:10-07

While refactoring the code to fetch account information, there was a problem that the page was rerendered indefinitely.

This is my custom hook to fetch account information.

//flattenTree returns an array of flattened Object that has 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,
    },
  }
}

And this is the component that uses the custom hook above.

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>
      ))}
      {/*
      elision
      */}
    </li>
  )
}

It seems that useFetchAccounts is called infinitely and because of that my component renders infinitely.

So, I thought there was a problem with how to use the custom hook, and moved all the logic inside the custom hook into the component, and the problem was solved.

Why did that happen?

And how modify or refactor the code?

This is an codesandbox example that reproduces the problem.

Edit msw-react-example (forked)

CodePudding user response:

Be careful with the useEffect because when you use it and specify the next for example:

useEffect(() => {
    if (accountTreeA) {
      //Flatten a tree object that has children
      setAccountsA(flattenTree(accountTreeA))
    }
  }, [accountTreeA])
  useEffect(() => {
    if (accountTreeB) {
      setAccountsB(flattenTree(accountTreeB))
    }
  }, [accountTreeB])

It will be called when accountTreeA and accountTreeB change. If you for example use, a useEffect with accountTreeA and inside the useEffect update the value with setAccountTreeA you are doing an infinite loop because it will be updating all time.

I don't know if I'm explaining well, but I want to tell you that useEffect is listening when accountTreeA or B has new updates to be called another time.

I hope that this information helps you.

Kind regards, Fernando

CodePudding user response:

You can use useMemo to memorize the values. As you return an object with {} so it will assume the object has changed to a new location.

return useMemo(
  () => ({
  accounts: {
    accountsA,
    accountsB,
  },
  accountIds: {
    accountIdsA,
    accountIdsB,
  },
}), [accountsA, accountsB, accountIdsA, accountIdsB]);

Updating answer from the comment in question.

  • Related