如何避免每次 select/de-select 行时 react-native FlatList 重新渲染
How to avoid react-native FlatList re-render every time I select/de-select a row
见Reproducible demo or code。
我尝试在反应本机 FlatList 上构建一个简单的 selectable 列表。功能很简单:FlatList 中的每一行都是 selectable。如果某行尚未 select 编辑,则单击该行将 select 它;如果该行已经 select 编辑,单击该行将取消 select 它。
我遇到的问题是每次点击一行,所有的行都会重新渲染,这可以从日志中看出(比如"rendering item id=cameron.nguyen@example.com, selected=false")。我想避免重新渲染未更改的行,因为重新渲染可能很昂贵(在我想加载非常大的图像或列表很长的情况下),但没有弄清楚如何去做。 <MomoizedItem />
和<MemoizedItem2 />
我都试过利用React.memo
,但前者根本不改变重新渲染行为,而后者使应用程序表现得很奇怪,你可以试试通过将 <Item />
替换为其中之一来查看效果。我还尝试使用 onClickCallBack
而不是 onClick
,但它也无济于事。
我使用 React.memo
或 React.useCallBack
不正确吗?我能做些什么来满足需求?谢谢。
如果代码 link 过期,请粘贴以下代码:
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
}
});
博览会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
是一个好的开始。
重新渲染的发生是因为 onClickUseCallBack
的实现方式。看,你有 selectedItems
作为 useCallback
的第二个参数,每次你 select/deselect 一个项目,selectedItems
改变,这会导致创建一个新的 onClickUseCallBack
然后传递给每个项目,这会破坏 memo
并导致每个项目重新呈现。
要解决此问题,您需要从 useCallback
的第二个参数中删除 selectedItems
,然后为了避免状态值过时(由于闭包的工作原理),请使用函数形式状态 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
完整代码
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
}
});
见Reproducible demo or code。
我尝试在反应本机 FlatList 上构建一个简单的 selectable 列表。功能很简单:FlatList 中的每一行都是 selectable。如果某行尚未 select 编辑,则单击该行将 select 它;如果该行已经 select 编辑,单击该行将取消 select 它。
我遇到的问题是每次点击一行,所有的行都会重新渲染,这可以从日志中看出(比如"rendering item id=cameron.nguyen@example.com, selected=false")。我想避免重新渲染未更改的行,因为重新渲染可能很昂贵(在我想加载非常大的图像或列表很长的情况下),但没有弄清楚如何去做。 <MomoizedItem />
和<MemoizedItem2 />
我都试过利用React.memo
,但前者根本不改变重新渲染行为,而后者使应用程序表现得很奇怪,你可以试试通过将 <Item />
替换为其中之一来查看效果。我还尝试使用 onClickCallBack
而不是 onClick
,但它也无济于事。
我使用 React.memo
或 React.useCallBack
不正确吗?我能做些什么来满足需求?谢谢。
如果代码 link 过期,请粘贴以下代码:
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
}
});
博览会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
是一个好的开始。
重新渲染的发生是因为 onClickUseCallBack
的实现方式。看,你有 selectedItems
作为 useCallback
的第二个参数,每次你 select/deselect 一个项目,selectedItems
改变,这会导致创建一个新的 onClickUseCallBack
然后传递给每个项目,这会破坏 memo
并导致每个项目重新呈现。
要解决此问题,您需要从 useCallback
的第二个参数中删除 selectedItems
,然后为了避免状态值过时(由于闭包的工作原理),请使用函数形式状态 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
完整代码
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
}
});