每当我选择/取消选择一行时,如何避免反应式FlatList重新呈现

时间:2020-02-22 20:23:02

标签: reactjs react-native react-hooks react-native-flatlist

请参见Reproducible democode

我尝试在本机FlatList上构建一个简单的可选列表。功能很简单:FlatList中的每一行都是可选的。如果尚未选择行,则单击该行将其选中;否则,请单击该行。如果已选择该行,则单击该行将取消选择它。

我面临的问题是,每当我单击一行时,所有行都将重新呈现,这可以从日志中得知(例如“呈现项id = cameron.nguyen @ example.com,selected = false ”)。我想避免重新渲染未更改的行,因为重新渲染可能会很昂贵(在我要加载非常大的图像或列表很长的情况下),但是却没有弄清楚该怎么做。我已经利用<MomoizedItem />尝试了<MemoizedItem2 />React.memo,但是前者根本不改变重新渲染的行为,而后者使应用程序的行为很奇怪,您可以尝试通过将<Item />替换为其中之一来查看效果。我还尝试过在onClickCallBack上使用onClick,但这也无济于事。

我使用React.memoReact.useCallBack的方式不正确吗?我该怎么做才能满足需要?谢谢。

如果代码链接过期,请粘贴以下代码:

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

{
  "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
}

1 个答案:

答案 0 :(得分:1)

MomoizedItem + onClickUseCallBack是一个好的开始。

由于如何实现onClickUseCallBack,因此发生了重新渲染。瞧,您有selectedItems作为useCallback的第二个参数,每次您选择/取消选择一项selectedItems都会改变,这会导致创建新的onClickUseCallBack,然后传递给每个项目,中断memo并导致每个项目重新呈现。

要解决此问题,您需要从selectedItems的第二个参数中删除useCallback,然后避免使用陈旧的状态值(由于闭包的工作原理),请使用状态设置器的功能形式拥有新的价值。

  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
  }
});