title: How to avoid react-native FlatList re-render every time I select/de-select a row
body: <p>See <a href="https://snack.expo.io/@evanchan/selectable-flatlist" rel="nofollow noreferrer">Reproducible demo</a> or <a href="https://github.com/YikSanChan/flatlist-selectable-searchable/blob/echan/experiement-memo/App.js" rel="nofollow noreferrer">code</a>.</p>
  
  <p>I try to build a simple selectable list upon react-native FlatList. The feature is simple: each row in the FlatList is selectable. If a row is not yet selected, clicking the row will select it; If the row is already selected, clicking the row will de-select it.</p>
  
  <p>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"). 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 <code>&lt;MomoizedItem /&gt;</code> and <code>&lt;MemoizedItem2 /&gt;</code> by leveraging <code>React.memo</code>, 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 <code>&lt;Item /&gt;</code> with one of them to see the effect. I also tried to use <code>onClickCallBack</code> over <code>onClick</code>, but it does not help either.</p>
  
  <p>Am I using <code>React.memo</code> or <code>React.useCallBack</code> incorrectly? What can I do to meet the need? Thank you.</p>
  
  <p>In case the code link expires, paste the code below:</p>
  
  <pre class="lang-js prettyprint-override"><code>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 }) =&gt; {
    console.log(`rendering item id=${id}, selected=${selected}`);
    return (
      &lt;ListItem
        title={title}
        leftAvatar={{ source: { uri: avatarUrl } }}
        containerStyle={[
          styles.item,
          { backgroundColor: selected ? "#6e3b6e" : "#f9c2ff" }
        ]}
        underlayColor="transparent"
        onPress={() =&gt; onClick(id)}
      /&gt;
    );
  };
  function itemEq(prevItem, nextItem) {
    return prevItem.id === nextItem.id &amp;&amp; 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 }) =&gt; {
    console.log("rendering items");
    // Replace &lt;Item /&gt; with &lt;MemoizedItem /&gt; or &lt;MemoizedItem2 /&gt; to see effect
    const _renderItem = ({ item }) =&gt; (
      &lt;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}
      /&gt;
    );
    return (
      &lt;FlatList
        data={data}
        renderItem={_renderItem}
        keyExtractor={item =&gt; item.email}
        extraData={selectedItems}
      /&gt;
    );
  };
  
  const App = () =&gt; {
    const [items, setItems] = useState([]);
    const [selectedItems, setSelectedItems] = useState(Set());
  
    useEffect(() =&gt; {
      const fetchData = async () =&gt; {
        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 =&gt; {
      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 =&gt; {
        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 (
      &lt;SafeAreaView style={styles.container}&gt;
        &lt;Items data={items} selectedItems={selectedItems} onClick={onClick} /&gt;
        &lt;Button
          title="Print"
          onPress={() =&gt; console.log(`Printing selected items ${selectedItems}`)}
        /&gt;
      &lt;/SafeAreaView&gt;
    );
  };
  
  export default App;
  
  const styles = StyleSheet.create({
    container: {
      flex: 1,
      marginTop: Constants.statusBarHeight,
      marginHorizontal: 16
    },
    item: {
      backgroundColor: "#f9c2ff",
      padding: 20,
      marginVertical: 8
    }
  });
  </code></pre>
  
  <p>expo package.json</p>
  
  <pre><code>{
    "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
  }
  </code></pre>