Я пытаюсь понять memo
и useCallback
и привел этот минимальный пример:
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>)
}
Это работает нормально, пока я не изменю count
, нажав Button
.
и это дает ошибку:
Ошибка: отрисовано меньше хуков, чем ожидалось. Это может быть вызвано случайным оператором раннего возврата.
Я не понимаю, что я делаю не так?
Разве мне не нужно использовать useCallback()
, чтобы <Item>
не отображался повторно из-за того, что «новая функция» (onClicked) каждый раз передается в качестве реквизита?
Как избежать повторного рендеринга каждый раз при выборе нового элемента?
Редактировать
Основываясь на комментарии Конрада, я использовал useMemo
вот так:
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>)
}
Но это вызывает больше вопросов, чем решает:
Как избежать использования двух циклов? Один для <Item>
, другой для useMemo
Как передать аргументы func_arr[i]
?
🤔 А знаете ли вы, что...
С помощью JavaScript можно создавать клиентские приложения для мобильных устройств с использованием фреймворков, таких как React Native и NativeScript.
Не совсем решение (скорее обходной путь), но можно передать саму функцию useCallback
как таковую:
import { memo, useCallback, useMemo, useState } from "react";
import { Button, Text, TouchableOpacity, SafeAreaView } from "react-native";
const Item = memo(({ isSelected, index, onClicked }) => {
console.info("Render Item", index)
return (<TouchableOpacity onPress = {() => onClicked(index)}>
<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 changeSelected = useCallback((index) => {
setSelected(index)
}, [])
let items = []
for (let i = 0; i < count; i++) {
items.push(
<Item
key = {i}
index = {i}
isSelected = {selected == i}
onClicked = {changeSelected}
/>
)
}
return (<SafeAreaView>
<Button title = "Change to 5" onPress = {() => { setCount(5); setSelected(1) }} />
{items}
</SafeAreaView>)
}
Здесь есть пара вещей: реакция ожидает, что вы всегда будете использовать одни и те же хуки, хуки не должны использоваться условно или внутри цикла, который может измениться.
В вашем случае, когда вы измените значение «count», вы будете использовать «useCallback» на один раз меньше, поэтому вы получите это сообщение об ошибке.
Хорошей новостью является то, что ваш обходной путь на самом деле правильный, позвольте мне объяснить: перехват «useCallback» используется для предотвращения повторной компиляции Reat функции каждый раз при обновлении компонента.
При этом вы хотите определить функцию, которая в вашем случае всегда будет одинаковой:
(index) => { setSelected(index) }
Эта функция всегда будет вести себя одинаково, поэтому ее не нужно создавать снова и снова. Затем вы используете useCallback, чтобы иметь возможность использовать эту функцию везде, где она вам нужна.
Отредактировано, чтобы добавить пример на основе комментария:
Просто чтобы уточнить: в вашем примере я, вероятно, не стал бы использовать ни один из этих хуков, потому что повторный рендеринг не так уж и дорог, но я понимаю, что это пример, и вы пытаетесь научиться их использовать. Итак, позвольте мне показать вам, что бы я сделал, чтобы использовать оба хука в этом примере:
import { memo, useCallback, useMemo, useState } from "react";
import { Button, Text, TouchableOpacity, SafeAreaView } from "react-native";
export default function App() {
const [selected, setSelected] = useState(0)
const [count, setCount] = useState(10)
// We use useCallback to save this function and stop re-building it every time the component updates
const callbackFunction = useCallback((index) => setSelected(i), [])
// Since we have callbackFunction and we can pass a parameter, we remover the first loop
// If you don't want to pass all those parameters (which is pretty usual if you are adding a child component),
// you can add another useCallback it inside the component like this:
const getMemoizedItemComponent = useCallback((index) => {
const isSelected = index === selected
return (<TouchableOpacity onPress = {() => callbackFunction(index)}>
<Text style = {{ backgroundColor: (isSelected ? "red" : "transparent"), paddingVertical: 10 }}>{index}</Text>
</TouchableOpacity>)
}, [callbackFunction])
let items = []
for (let i = 0; i < count; i++) // loop 2
items.push(
getMemoizedItemComponent(i)
)
return (<SafeAreaView>
<Button title = "Change to 5" onPress = {() => { setCount(5); setSelected(1) }} />
{items}
</SafeAreaView>)
}
Еще один комментарий. Я не думаю, что необходимость передавать эти три параметра в «Item» — это большая проблема. Есть способы сделать это немного лучше (например, useContext), но здесь это не поможет. Обычно, если у вас есть компонент, отображающий дочерние компоненты на основе списка, вам придется передать каждому из них кучу реквизитов.