Home > database >  React hooks depends on another state can cause infinite loop. Why is that?
React hooks depends on another state can cause infinite loop. Why is that?

Time:03-07

I have two hooks, one of which depends on the other state. I wrote two patterns to handle it. When useEffect is in custom hooks (and I want to hide useEffect into a custom hook), it causes an infinite loop. Why is that? Thanks.

Infinite loop version

hooks.ts

  const useUpdate = (initialValue) => {
    [selectedItem, setSelectedItem] = useState(initialValue)
    useEffect(() => {
      setSelectedItem(initialValue)
    }, [initialValue])
    const update = () => {...}
    return update
  }

MyComponent.ts

const MyComponent = () => {
  const [selectedItem, setSelectecItem] = useSelectItem()
  const update = useUpdate(selectedItem)
  return (<div>...</div>)
}

Success version

hooks.ts

const useUpdate = (initialValue) => {
  [selectedItem, setSelectedItemInUpdate] = useState(initialValue)
  const update = () => {...}
  return {update, setSelectedItemInUpdate}
}

MyComponent.tsx

const MyComponent = () => {
  const [selectedItem, setSelectecItem] = useSelectItem()
  const {update, setSelectedItemInUpdate} = useUpdate(selectedItem)
  useEffect(() => {
    setSelectedItemInUpdate(selectedItem)
  }, [selectedItem])
  return (<div>...</div>)
}

Edited (2022/03/07) to answer a comment, contents of custom hooks are shown below. They are still not entire source code, but it can give much more context. useUpdate's return type isn't consistent with the above description.

const useSelectItem = (initialValue: Item[] =[]) => {
  // Select and unselect one item.
  const [selectedItem, setSelectedItem] = useState<Item[]>(initialValue)
  const select = (item: Item) => {
    if (selectedItem.at(0) === item) {
      // if already selected, unselect it.
      selectedItem([])
    } else {
      selectedItem([item])
    }
  }
  return [selectedItem, select]
}

// useUpdate should operate on a selectedItem
const useUpdate = () => {
  // keep shallow copy of selectedItem to edit locally.
  const [item, setItem] = useState<Item|undefined>(undefined)
  const onChangeName = (event: React.ChangeEvent<HTMLInputElement>) => {
    setItem({...item, {[name]: event.target.value}
  }
  const update = async () => {
    // write to firestore provided by Firebase, Google.
    await updateDoc(doc(db, 'items', item.id), item)
  }
  //  ... and many other functions to operate on input field.
  return {onChangeName, update, and many others}

}

Of course these hooks can be combined into one hook, but I think I have to separate them so that they have a single responsibility.

CodePudding user response:

I think it is happening because of useEffect dependency array. In the infinite loop version of useEffect, initialValue is changing constantly, and it is causing the infinite loop.

Try changing it to the below code.

   const useUpdate = (initialValue) => {
       [selectedItem, setSelectedItem] = useState(initialValue)
       useEffect(() => {
       setSelectedItem(initialValue)
       //eslint-disable-next-line
       }, [])
       const update = () => {...}
       return update
  }

//eslint-disable-next-line won't give you warnings because of empty `dependency array'

CodePudding user response:

As Dharmik Patel's answer stated, the issue is likely caused by the dependency array. Another way you can get around the issue, without disabling eslint is to useMemo.


   const useUpdate = (initialValue) => {
       const initial = useMemo(() => initialValue, [initialValue])
       [selectedItem, setSelectedItem] = useState(initialValue)
       useEffect(() => {
       setSelectedItem(initial)
       }, [initial])
       const update = () => {...}
       return update
  }

The useMemo will ensure that re-renders only occur if the value of initialValue changes. However this article explains an even better way to do this than using useMemo.

  • Related