javascriptreactjsreact-nativereact-native-flatlist

React Native Flatlist: Is it posible to avoid Flatlist items re render if one item's callback result is changed?


memo, useMemo and useCallback are not helping to solve the re-rendering. Now I am confused is it even possible or not. I am trying hard to prevent flatlist from re-rendering all items when i select one item.

The problem is when I check if item already exist in the state or not and that's causing the re rendering issue.

Parent:

const FormScreen = () => {
  
  // const [state, dispatch] =  useReducer(formReducer, initialState);
  const [ bucket, setBucket ] = useState([]);
  
  logs("rendering... FORM SCREEN");

  return (
    <View style={formStyles.formContainer}>

        <InputPackName />
        <InputCreatorName />
        <SelectedImageView selected={state.selected} dispatch={dispatch}/>
        <GalleryView 
          bucket={bucket} 
          setBucket={setBucket} 
            />
        <PackSubmitButton />

    </View>
  )

}


export default FormScreen;

GalleryView:

const GalleryView = ({ bucket, setBucket }) => {
    const [ data, setData ] = useState(); // [{uri:'', id:''},...]


    // populate dat for flatlist
    useEffect(() => {
          setData(images)
    }, []);
    
    // check if bucket is updating
    useEffect(() => {
      log({bucket}); // fine. ["0", "4", ...] adds removes ids of item
    }, [bucket]);


    // call back prevents re render without it all item get re render on onPress any item
    const renderItem = useCallback(({ item }) => (
      <GalleryItem
        item={item}
        // handlePress={handlePress} // moved the handle to item as bucket, setbucket for handeling logic inside item component
        bucket={bucket}
        setBucket={setBucket}
      />
  ),[bucket]) 


    console.log("render view")
    return (
        <View style={[styles.container, {backgroundColor:theme.colors.background}]}>
            <FlatList
                ref={flatListRef}
                data={data}
                // extraData={state.selected}
                // keyExtractor={(item, index) => index.toString() } // tried with keyExtrator
                numColumns={4}
                renderItem={renderItem}
                style={styles.flatList}
                columnWrapperStyle = {styles.columnWrapperStyle}
                // getItemLayout={getItemLayout}
                removeClippedSubviews 
                initialNumToRender={20}
                
            />
        </View>
    );
};

export default React.memo(GalleryView);

GalleryItem:

const GalleryItem = ({bucket, setBucket, item, handlePress, dispatch  }) => {
  console.log("Gallery Item render ..",item.uri);

  const isSelected = useMemo(
    () => bucket.includes(item.id), 
    [item.id]
  );
  const memoizedHandlePress = useCallback(() => {
    log(" state onPress : ", bucket) // [] empty as it was initial value. never get updated state here

    if (!isSelected) {
      setBucket(p=>[...p, item.id]);
      log(" SELECT_IMAGE ✅ ", bucket, isSelected)
       
    } else {
      log(" DESELECT_IMAGE ❌")
      setBucket(prevBucket => prevBucket.filter(i => i !== item.id));
    }
  }, [bucket]);
  
  return (
    <Pressable
      style={[styles.item]}
      onPress={memoizedHandlePress}
    >
      <FastImage
        style={styles.image}
        source={{ uri: item.uri }}
        resizeMode={FastImage.resizeMode.contain}
      />
    </Pressable>
  );
};

export default React.memo(GalleryItem);

bucket = [] get filled on Press and I can check that in useEffect of GalleryView but the bucket state is same (empty) in GalleryItem as we are using callback. and If add dependency all items will re render and everything is fine but then I wont be able to check if current item is already sleected or not. I want to know if item is selected or not for applying style and toggle select-deselect item because The selected item are being used by sibling component for showing selected items.


Solution

  • Found the solution but I don't know if it is efficient way or is there another proper methods to do it.

    This solution surely skipping rendering for all items except one which I press.

    Solution: When React.memo(component, callback) is used, the way in which the rendering should be done is specified through a callback function. For every item, this function is called and it runs before rendering takes place. It decides whether any particular item should not re-render at all.

    Example Code: using the second argument in React.memo(component, callback) for Custom equality check of props.

    const GalleryItem = ({bucket, setBucket, item, handlePress, dispatch  }) => {
    
      const onPress = () => {
        const isSelected = bucket.includes(item.id)
    
        if (!isSelected) {
          setBucket(p=>[...p, item.id]);
          // log(" SELECT_IMAGE ✅ ") 
        } else {
          setBucket(prevBucket => prevBucket.filter(i => i !== item.id));
          // log(" DESELECT_IMAGE ❌")
        }
      }
      
      return (
        <Pressable onPress={onPress}>
          ...
        </Pressable>
      );
    };
    
    // should update or skip the render for this item
    const shouldSkip = (prev, next) => (
      next.bucket.includes(prev.item.id) === prev.bucket.includes(prev.item.id) 
    );
    
    export default React.memo(GalleryItem, shouldSkip);
    

    Explanation: arguments prev and next contain all props passed to Item component.

    We need to check both state with current item id.

    Skip re-rendering if

    1. current id present in both prev and next state
      OR
    2. current id absent in both prev and next state

    Here is log. 0th item is already selected and then we select 1st item so it should re-render 1st item and skip 0th and 3rd item

      console.log(prev.item.id," < ⬅️ ",prev.bucket);
      console.log(next.item.id," ➡️ > ",next.bucket);
      console.log(equal?"skip ":"render ");
      console.log("---------------------------- ");
    

    enter image description here