I’m trying to understand memo
and useCallback
and I’ve made this minimal example:
import { memo, useCallback, useState } from "react";
import { Button, Text, TouchableOpacity, SafeAreaView } from "react-native";
const Item = memo(({ isSelected, index, onClicked }) => {
return (<TouchableOpacity onPress={onClicked}>
<Text style={{ backgroundColor: (isSelected ? "red" : "transparent"), paddingVertical: 10 }}>{index}</Text>
</TouchableOpacity>)
})
export default function App() {
const [selected, setSelected] = useState(0)
const [count, setCount] = useState(10)
let items = []
for (let i = 0; i < count; i++)
items.push(
<Item
key={i}
index={i}
isSelected={selected == i}
onClicked={useCallback(() => setSelected(i), [])}
/>
)
return (<SafeAreaView>
<Button title="Change to 5" onPress={() => { setCount(5); setSelected(1) }} />
{items}
</SafeAreaView>)
}
This works fine until I change the count
by clicking on Button
and It gives the error:
Error: Rendered fewer hooks than expected. This may be caused by an accidental early return statement.
I don’t understand what I’m doing wrong?
Don’t I have to use useCallback()
so that the <Item>
don’t get re rendered due to a "new function" (onClicked) being passed in as props every time?
How do I avoid re rendering every time a new item is selected?
Edit
Based on Konrad’s comment I utilized useMemo
like this:
import { memo, useCallback, useMemo, useState } from "react";
import { Button, Text, TouchableOpacity, SafeAreaView } from "react-native";
const Item = memo(({ isSelected, index, onClicked }) => {
return (<TouchableOpacity onPress={onClicked}>
<Text style={{ backgroundColor: (isSelected ? "red" : "transparent"), paddingVertical: 10 }}>{index}</Text>
</TouchableOpacity>)
})
export default function App() {
const [selected, setSelected] = useState(0)
const [count, setCount] = useState(10)
const func_arr = useMemo(() => {
let arr = []
for (let i = 0; i < count; i++) // loop 1
arr.push(() => setSelected(i))
return arr
},[count])
let items = []
for (let i = 0; i < count; i++) // loop 2
items.push(
<Item
key={i}
index={i}
isSelected={selected == i}
onClicked={func_arr[i]} // How do I pass arguments to this?
/>
)
return (<SafeAreaView>
<Button title="Change to 5" onPress={() => { setCount(5); setSelected(1) }} />
{items}
</SafeAreaView>)
}
But this raises more questions then it solves:
-
How do I avoid using 2 loops? One for the
<Item>
& the other foruseMemo
-
How do I pass arguments to
func_arr[i]
?
2
Answers
Not exactly a solution (more of a workaround) but it's possible to pass the
useCallback
function itself as such:A couple of things here, reacts expects you to allways use the same hooks, hooks should not be conditionally used, nor used inside a loop that can change.
In your case, when you change the value of "count", you will use "useCallback" one less time, that’s why you get that error message.
The good news is your workaround is actually correct, let me explain, the "useCallback" hook is used to prevent Reat to re-compiling the function everytime the component gets updated.
That being said, what you want to do is define a function that is going to always be the same, in your case:
That function will allways behave the same, so it does not need to be built over and over. Then, you use useCallback to be able to use that function everywhere you need it.