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.