[英]How to avoid react-native FlatList re-render every time I select/de-select a row
See Reproducible demo or code .请参阅可重现的演示或代码。
I try to build a simple selectable list upon react-native FlatList.我尝试在 react-native FlatList 上构建一个简单的可选列表。 The feature is simple: each row in the FlatList is selectable.
功能很简单:FlatList 中的每一行都是可选的。 If a row is not yet selected, clicking the row will select it;
如果某行尚未被选中,则单击该行将 select 它; If the row is already selected, clicking the row will de-select it.
如果该行已被选中,单击该行将取消选择它。
The problem I am faced with is every time I click a row, all rows are re-rendered, which can be told from the log (such as "rendering item id=cameron.nguyen@example.com, selected=false").我面临的问题是每次单击一行时,所有行都会重新渲染,这可以从日志中得知(例如“渲染项目id=cameron.nguyen@example.com,selected=false”)。 I want to avoid the re-render of unchanged rows because the re-render can be expensive (in the case when I want to load very large images, or when the list is very long), but didn't figure out how to.
我想避免重新渲染未更改的行,因为重新渲染可能很昂贵(在我想加载非常大的图像或列表很长的情况下),但没有弄清楚如何去做。 I have tried both
<MomoizedItem />
and <MemoizedItem2 />
by leveraging React.memo
, but the former does not change the re-render behavior at all, while the latter makes the app behaves very weird, you can try by replacing <Item />
with one of them to see the effect.我已经通过利用
React.memo
尝试了<MomoizedItem />
和<MemoizedItem2 />
,但前者根本没有改变重新渲染行为,而后者使应用程序表现得很奇怪,你可以尝试替换<Item />
用其中一个看看效果。 I also tried to use onClickCallBack
over onClick
, but it does not help either.我还尝试在
onClick
onClickCallBack
但它也无济于事。
Am I using React.memo
or React.useCallBack
incorrectly?我是否错误地使用
React.memo
或React.useCallBack
? What can I do to meet the need?我能做些什么来满足需求? Thank you.
谢谢你。
In case the code link expires, paste the code below:如果代码链接过期,请粘贴以下代码:
import React, { memo, useEffect, useState } from "react";
import { SafeAreaView, FlatList, StyleSheet } from "react-native";
import Constants from "expo-constants";
import { Set } from "immutable";
import { Button, ListItem } from "react-native-elements";
import axios from "axios";
const Item = ({ id, title, avatarUrl, selected, onClick }) => {
console.log(`rendering item id=${id}, selected=${selected}`);
return (
<ListItem
title={title}
leftAvatar={{ source: { uri: avatarUrl } }}
containerStyle={[
styles.item,
{ backgroundColor: selected ? "#6e3b6e" : "#f9c2ff" }
]}
underlayColor="transparent"
onPress={() => onClick(id)}
/>
);
};
function itemEq(prevItem, nextItem) {
return prevItem.id === nextItem.id && prevItem.selected === nextItem.selected;
}
// Does not make a difference, every time a row is clicked, all rows are re-rendered
const MemoizedItem = memo(Item);
// Make some difference but the behavior looks very weird. Try click around and see the log
const MemoizedItem2 = memo(Item, itemEq);
const Items = ({ data, selectedItems, onClick }) => {
console.log("rendering items");
// Replace <Item /> with <MemoizedItem /> or <MemoizedItem2 /> to see effect
const _renderItem = ({ item }) => (
<Item
id={item.email}
title={`${item.name.title} ${item.name.first} ${item.name.last}`}
avatarUrl={item.picture.thumbnail}
selected={selectedItems.has(item.email)}
onClick={onClick}
/>
);
return (
<FlatList
data={data}
renderItem={_renderItem}
keyExtractor={item => item.email}
extraData={selectedItems}
/>
);
};
const App = () => {
const [items, setItems] = useState([]);
const [selectedItems, setSelectedItems] = useState(Set());
useEffect(() => {
const fetchData = async () => {
console.log("fetching data");
// Read 5 random users back
// Each user is like this:
// {
// "gender":"male",
// "name":{
// "title":"Mr",
// "first":"Harley",
// "last":"Zhang"
// },
// "location":{
// "street":{
// "number":6470,
// "name":"Buckleys Road"
// },
// "city":"Palmerston North",
// "state":"Manawatu-Wanganui",
// "country":"New Zealand",
// "postcode":90911,
// "coordinates":{
// "latitude":"66.2907",
// "longitude":"-18.0881"
// },
// "timezone":{
// "offset":"+8:00",
// "description":"Beijing, Perth, Singapore, Hong Kong"
// }
// },
// "email":"harley.zhang@example.com",
// "login":{
// "uuid":"6fda195e-3e63-476c-84d0-7c577c7b74f9",
// "username":"smallbear541",
// "password":"daisy1",
// "salt":"p6AmByUq",
// "md5":"0358f2385a9936369adc89b9233f037b",
// "sha1":"8decc817cf32ca6e58814502bb3e54152208c5b5",
// "sha256":"96ff7627348250646edd31238504271840a0cb6aaac293782f7eec1a6f884c07"
// },
// "dob":{
// "date":"1987-12-07T13:00:15.244Z",
// "age":33
// },
// "registered":{
// "date":"2008-01-23T19:33:01.672Z",
// "age":12
// },
// "phone":"(474)-743-9612",
// "cell":"(539)-021-1315",
// "id":{
// "name":"",
// "value":null
// },
// "picture":{
// "large":"https://randomuser.me/api/portraits/men/49.jpg",
// "medium":"https://randomuser.me/api/portraits/med/men/49.jpg",
// "thumbnail":"https://randomuser.me/api/portraits/thumb/men/49.jpg"
// },
// "nat":"NZ"
// }
const results = await axios("https://randomuser.me/api/?results=5");
setItems(results.data.results);
};
fetchData();
}, []);
const onClick = id => {
const newSelectedItems = selectedItems.has(id)
? selectedItems.delete(id)
: selectedItems.add(id);
console.log(`selected items=${JSON.stringify(selectedItems, null, 2)}`);
console.log(
`new selected items=${JSON.stringify(newSelectedItems, null, 2)}`
);
setSelectedItems(newSelectedItems);
}
// Does not help
const onClickUseCallBack = React.useCallback(
id => {
const newSelectedItems = selectedItems.has(id)
? selectedItems.delete(id)
: selectedItems.add(id);
console.log(`selected items=${JSON.stringify(selectedItems, null, 2)}`);
console.log(
`new selected items=${JSON.stringify(newSelectedItems, null, 2)}`
);
setSelectedItems(newSelectedItems);
},
[selectedItems]
);
return (
<SafeAreaView style={styles.container}>
<Items data={items} selectedItems={selectedItems} onClick={onClick} />
<Button
title="Print"
onPress={() => console.log(`Printing selected items ${selectedItems}`)}
/>
</SafeAreaView>
);
};
export default App;
const styles = StyleSheet.create({
container: {
flex: 1,
marginTop: Constants.statusBarHeight,
marginHorizontal: 16
},
item: {
backgroundColor: "#f9c2ff",
padding: 20,
marginVertical: 8
}
});
expo package.json expo package.json
{
"main": "node_modules/expo/AppEntry.js",
"scripts": {
"start": "expo start",
"android": "expo start --android",
"ios": "expo start --ios",
"web": "expo start --web",
"eject": "expo eject"
},
"dependencies": {
"axios": "^0.19.2",
"expo": "~36.0.0",
"immutable": "^4.0.0-rc.12",
"react": "~16.9.0",
"react-dom": "~16.9.0",
"react-native": "https://github.com/expo/react-native/archive/sdk-36.0.0.tar.gz",
"react-native-elements": "^1.2.7",
"react-native-web": "~0.11.7"
},
"devDependencies": {
"@babel/core": "^7.0.0",
"babel-preset-expo": "~8.0.0"
},
"private": true
}
MomoizedItem
+ onClickUseCallBack
is a good start. MomoizedItem
+ onClickUseCallBack
是一个好的开始。
The re-rendering happens because of how onClickUseCallBack
is implemented.重新呈现的发生是因为
onClickUseCallBack
的实现方式。 See, you have selectedItems
as the second parameter to useCallback
, every time you select/deselect an item, selectedItems
change, which causes a new onClickUseCallBack
to be created an then passed to every item, which break memo
and causes every item to re-render.看,您将
selectedItems
作为useCallback
的第二个参数,每次选择/取消选择一个项目时, selectedItems
都会发生变化,这会导致创建一个新的onClickUseCallBack
然后传递给每个项目,这会破坏memo
并导致每个项目重新呈现.
To fix this, you need to remove selectedItems
from the second parameter of useCallback
, then to avoid having stale state value (due to how closure works), use the functional form of the state setter to have fresh value.要解决此问题,您需要从
useCallback
的第二个参数中删除selectedItems
,然后为避免 state 值过时(由于闭包的工作方式),使用 state setter 的函数形式以获得新值。
const onClickUseCallBack = React.useCallback(
id => {
setSelectedItems((selectedItems) => {
const newSelectedItems = selectedItems.has(id)
? selectedItems.delete(id)
: selectedItems.add(id);
return newSelectedItems
});
},
[]
);
https://snack.expo.io/HJXkV!Q48 https://snack.expo.io/HJXkV!Q48
import React, { memo, useEffect, useState } from "react";
import { SafeAreaView, FlatList, StyleSheet } from "react-native";
import Constants from "expo-constants";
import { Set } from "immutable";
import { Button, ListItem } from "react-native-elements";
import axios from "axios";
const Item = ({ id, title, avatarUrl, selected, onClick }) => {
console.log(`rendering item id=${id}, selected=${selected}`);
return (
<ListItem
title={title}
leftAvatar={{ source: { uri: avatarUrl } }}
containerStyle={[
styles.item,
{ backgroundColor: selected ? "#6e3b6e" : "#f9c2ff" }
]}
underlayColor="transparent"
onPress={() => onClick(id)}
/>
);
};
function itemEq(prevItem, nextItem) {
return prevItem.id === nextItem.id && prevItem.selected === nextItem.selected;
}
// Does not make a difference, every time a row is clicked, all rows are re-rendered
const MemoizedItem = memo(Item);
// Make some difference but the behavior looks very weird. Try click around and see the log
const MemoizedItem2 = memo(Item, itemEq);
const Items = ({ data, selectedItems, onClick }) => {
console.log("rendering items");
// Replace <Item /> with <MemoizedItem /> or <MemoizedItem2 /> to see effect
const _renderItem = ({ item }) => (
<MemoizedItem
id={item.email}
title={`${item.name.title} ${item.name.first} ${item.name.last}`}
avatarUrl={item.picture.thumbnail}
selected={selectedItems.has(item.email)}
onClick={onClick}
/>
);
return (
<FlatList
data={data}
renderItem={_renderItem}
keyExtractor={item => item.email}
extraData={selectedItems}
/>
);
};
const App = () => {
const [items, setItems] = useState([]);
const [selectedItems, setSelectedItems] = useState(Set());
useEffect(() => {
const fetchData = async () => {
console.log("fetching data");
// Read 5 random users back
// Each user is like this:
// {
// "gender":"male",
// "name":{
// "title":"Mr",
// "first":"Harley",
// "last":"Zhang"
// },
// "location":{
// "street":{
// "number":6470,
// "name":"Buckleys Road"
// },
// "city":"Palmerston North",
// "state":"Manawatu-Wanganui",
// "country":"New Zealand",
// "postcode":90911,
// "coordinates":{
// "latitude":"66.2907",
// "longitude":"-18.0881"
// },
// "timezone":{
// "offset":"+8:00",
// "description":"Beijing, Perth, Singapore, Hong Kong"
// }
// },
// "email":"harley.zhang@example.com",
// "login":{
// "uuid":"6fda195e-3e63-476c-84d0-7c577c7b74f9",
// "username":"smallbear541",
// "password":"daisy1",
// "salt":"p6AmByUq",
// "md5":"0358f2385a9936369adc89b9233f037b",
// "sha1":"8decc817cf32ca6e58814502bb3e54152208c5b5",
// "sha256":"96ff7627348250646edd31238504271840a0cb6aaac293782f7eec1a6f884c07"
// },
// "dob":{
// "date":"1987-12-07T13:00:15.244Z",
// "age":33
// },
// "registered":{
// "date":"2008-01-23T19:33:01.672Z",
// "age":12
// },
// "phone":"(474)-743-9612",
// "cell":"(539)-021-1315",
// "id":{
// "name":"",
// "value":null
// },
// "picture":{
// "large":"https://randomuser.me/api/portraits/men/49.jpg",
// "medium":"https://randomuser.me/api/portraits/med/men/49.jpg",
// "thumbnail":"https://randomuser.me/api/portraits/thumb/men/49.jpg"
// },
// "nat":"NZ"
// }
const results = await axios("https://randomuser.me/api/?results=5");
setItems(results.data.results);
};
fetchData();
}, []);
// Does not help
const onClickUseCallBack = React.useCallback(
id => {
setSelectedItems((selectedItems) => {
const newSelectedItems = selectedItems.has(id)
? selectedItems.delete(id)
: selectedItems.add(id);
return newSelectedItems
});
},
[]
);
return (
<SafeAreaView style={styles.container}>
<Items data={items} selectedItems={selectedItems} onClick={onClickUseCallBack} />
<Button
title="Print"
onPress={() => console.log(`Printing selected items ${selectedItems}`)}
/>
</SafeAreaView>
);
};
export default App;
const styles = StyleSheet.create({
container: {
flex: 1,
marginTop: Constants.statusBarHeight,
marginHorizontal: 16
},
item: {
backgroundColor: "#f9c2ff",
padding: 20,
marginVertical: 8
}
});
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.