ifelsething logoifelsething

Get Viewable List Items in React Native

Published On: 2024-07-29
Posted By: Harish

viewable list items in react native

We discussed scrolling to different list items using coordinates or programmatically scrolling to different list items through their indexes etc. In those scenarios, we have an index of the item to scroll to.

Now, we will do the opposite in this post, we will get an index of a list item, which is viewable on screen.

Let's discuss this scenario with an example. I will use a VirtualizedList component for this example but the below process is the same for FlatList and SectionList components.

Create A New Project

Create a new react-native project by using npx. Check documentation for creating a new react native project.

npx react-native@latest init VListRN

Example Implementation

We will create a simple vertical VirtualizedList with blocks. And we will get the viewable list items when certain conditions are matched.

Import and add VirtualizedList. Add basic VirtualizedList with color blocks. Check basic VirtualizedList implementation for more info.

//App.tsx
...
import { VirtualizedList } from 'react-native';
...
<VirtualizedList
  contentContainerStyle={styles.content_container}
  data={listData}
  getItemCount={getItemCount}
  getItem={getItem}
  keyExtractor={item => item.id + ""}
  renderItem={({ index }) => {
    return (
      <Item id={index} />
    )
  }}
/>
...

Complete code can be found at the bottom of this post.

If you run the app,

#for Android
npx react-native run-android

#for ios
npx react-native run-ios

A list of items can be seen with their indexes.

To get currently viewable items, we can use onViewableItemsChanged callback. This callback returns current viewable and changed items on screen.

With this callback, we need to use viewabilityConfig prop to configure the viewable items conditions..

viewabilityConfig

Before we get into the process, we have to discuss this viewabilityConfig prop.

This is like an object with conditions to treat an item as viewable only when those conditions are met. Let's discuss these properties in simple terms. You can find more at FlatList documentation(https://reactnative.dev/docs/flatlist#viewabilityconfig).

minimumViewTime : This is the minimum time to wait before considering an item as viewable. Components will wait this much time to return the viewable items in onViewableItemsChanged callback.

viewAreaCoveragePercentThreshold : An item should cover at least this much percentage of overall list viewport height or width to consider an item as viewable. Will discuss this at the end of this post.

itemVisiblePercentThreshold : An item should cover at least this much percentage of height or width in list view to consider it as a viewable item. Will discuss this at the end of this post.

waitForInteraction : This tells whether to call onViewableItemsChanged callback on load or to wait till the user interacts with the list.

If the list is vertical, then we will consider the height percentage of an item. If its horizontal, width percentage to be taken into consideration.

Add onViewableItemsChanged and viewabilityConfig

Add onViewableItemsChanged callback with arguments changed and viewableItems. And for viewabilityConfig, check below code.

...
const viewabilityConfig = {
  minimumViewTime: 500,
  //viewAreaCoveragePercentThreshold: 80,
  itemVisiblePercentThreshold: 80,
  waitForInteraction: true
};
const onViewableItemsChanged = ((info: InfoProp) => {
  const { viewableItems, changed } = info;
  console.log(viewableItems, changed)
});
...
<VirtualizedList
  ...
  viewabilityConfig={viewabilityConfig}
  onViewableItemsChanged={onViewableItemsChanged}
/>
...

According to our config properties, the list component waits till the user interacts with the list, meaning onViewableItemsChanged will return viewable items only when we interact with the list, in our case its scrolling.

We want to consider an item as a viewable item after 500 milliseconds of interaction or without interaction. In our case, after every interaction, it waits 500 milliseconds to call the callback.

Now comes the main part, viewAreaCoveragePercentThreshold: 80 means, consider an item as viewable, if it takes 80% of total viewable area of the list.

And itemVisiblePercentThreshold: 80 means, consider an item as viewable, if 80% of that item is viewable to the user. And please note that these two are not the same.

Any one of those two properties should be used. So, we will use itemVisiblePercentThreshold.

Now, reload the app and scroll the list to console log the viewable and changed items.

getting log of viewable items in react native
LOG  Viewable Items:  [{"index": 1, "isViewable": true, "item": {"color": "bisque", "id": 2}, "key": "2"}]
LOG  Changed Items:  [{"index": 0, "isViewable": false, "item": {"color": "bisque", "id": 1}, "key": "1"}]

From the above screenshot, even though index 0 and 1 items are on screen, index 1 meets our config condition of more than 80% of item height on screen. So index 1 is considered as viewable.

For better understanding, let's change the background color of the viewable items to green when itemVisiblePercentThreshold: 100. Also change waitForInteraction to false.

To change color, we will take the viewable items indexes in an array and change their background color to green if they are viewable.

import React, { useCallback, useRef, useState } from "react";
...
const viewabilityConfigForGreen = useRef({
  ...
  itemVisiblePercentThreshold: 100,
  waitForInteraction: false
});
...
const onViewableItemsChangedForGreen = useCallback(((info: InfoProp) => {
  const { viewableItems, changed } = info;
  const indexArray = viewableItems.map((item: ViewToken) => item.index)
  setChangeIndexForGreen(indexArray);
}), []);
...
<VirtualizedList
  ...
  viewabilityConfig={viewabilityConfigForGreen.current}
  onViewableItemsChanged={onViewableItemsChangedForGreen}
/>
...

Changed the name of the callback and config for ease. Please check complete code down below for item background color change code implementation.

In a few cases, we may get Changing viewabilityConfig on the fly is not supported error when we try to change a viewable item on the go. So, to avoid that, we have to memoize the viewabilityConfig and onViewableItemsChanged. We can use useRef or useCallback or useMemo hooks.

For this example, I'm using useRef and useCallback.

Now, re-run the metro builder and after rendering, if you scroll slowly, you can see that the item's background changes to green when a complete item is on screen.

change viewable items background color in react native

viewabilityConfigCallbackPairs

Till now we are changing only one item's background. But what if we want another set of viewable items with different property values?

In those cases, instead of those single config and callback, we can use viewabilityConfigCallbackPairs, which accepts an array of viewabilityConfig and onViewableItemsChanged objects.

Lets say, with green color, we want to change other items' background to red, when a minimum 10% of the item is visible to us.

So, add another set of viewabilityConfig and onViewableItemsChanged for red color. And add to viewabilityConfigCallbackPairs.

...
const viewabilityConfigCallbackPairs = useRef([
  {
    viewabilityConfig: viewabilityConfigForGreen,
    onViewableItemsChanged: onViewableItemsChangedForGreen
  },
  {
    viewabilityConfig: viewabilityConfigForRed,
    onViewableItemsChanged: onViewableItemsChangedForRed
  },
]);
...
<VirtualizedList
  ...
  //viewabilityConfig={viewabilityConfigForGreen.current}
  //onViewableItemsChanged={onViewableItemsChangedForGreen}
  viewabilityConfigCallbackPairs={viewabilityConfigCallbackPairs.current}
/>
...

For red color viewabilityConfig and onViewableItemsChanged, please refer to the complete code down below.

So, we are using viewabilityConfigCallbackPairs to get different viewable items when different conditions are matched. Here, we are using useRef for whole pairs, so remove useRef and useCallback hooks of green color.

Now, re-run the metro builder and scroll items slowly to see a red background for items which are above 10% viewable and change to green background when they are 100% viewable.

apply two configs for list items viewability

viewabilityConfig's viewAreaCoveragePercentThreshold and itemVisiblePercentThreshold

As we are changing the background color, now we can clearly see the difference between viewAreaCoveragePercentThreshold and itemVisiblePercentThreshold properties.

As we already used viewabilityConfigCallbackPairs, let's use this for a single pair of viewability.

...
const viewabilityConfigCallbackPairsForOnePair = useRef([
  {
    viewabilityConfig: viewabilityConfigForGreen,
    onViewableItemsChanged: onViewableItemsChangedForGreen
  }
]);
...
<VirtualizedList
  ...
  //viewabilityConfigCallbackPairs={viewabilityConfigCallbackPairs.current}
  viewabilityConfigCallbackPairs={viewabilityConfigCallbackPairsForOnePair.current}
/>
...

This is similar to passing individual viewabilityConfig prop and onViewableItemsChanged callback. Now re-run the metro builder, on render you will see that an item changes its background color to green when completely visible to us.

viewAreaCoveragePercentThreshold

Let's fix the height of the list to 400, add viewAreaCoveragePercentThreshold property to viewabilityConfigForGreen with 80%. Only use this property and comment itemVisiblePercentThreshold.

Now, re-run the app and on load, slowly scroll the items.

view area percentage threshold in item viewability

If you observe the above gif closely, an item background changes to green when the item's height occupies 80% of the height of listview,. And if you change the list view's height, you may get different viewable items.

So, irrespective of the item's height, only the list view's height is used when viewAreaCoveragePercentThreshold property is used. Also take note that, if an item is completely visible in the viewport, it is considered as viewable even if it does not qualify viewAreaCoveragePercentThreshold's percentage.

itemVisiblePercentThreshold

Now, comment viewAreaCoveragePercentThreshold and use itemVisiblePercentThreshold with 80% keeping the list view's height to 400.

Re-run the metro builder and after reload, slowly scroll the list.

item visible threshold in react native viewability

And in this gif, if you observe, the item's background turns green only when 80% of the item's height is visible to us. Here only the item's height is taken into consideration to output it as viewable.

So, in this way we can get viewable items in react native VirtualizedList or FlatList or SectionList.

Complete code of our example,

//App.tsx
import React, { useRef, useState } from "react";
import {
  Text,
  StyleSheet,
  SafeAreaView,
  StatusBar,
  View,
  VirtualizedList,
  ViewToken,
} from "react-native";

type ItemDataProp = {
  id: number;
  color: string
};

type InfoProp = {
  viewableItems: Array<ViewToken>;
  changed: Array<ViewToken>
};

let listData = [
  {
    id: 1,
    color: 'bisque'
  },
  {
    id: 2,
    color: 'bisque'
  },
  {
    id: 3,
    color: 'bisque'
  },
  {
    id: 4,
    color: 'bisque'
  },
  {
    id: 5,
    color: 'bisque'
  }
];

export default function App() {
  const [changeIndexForGreen, setChangeIndexForGreen] = useState<Array<number | null>>([]);
  const [changeIndexForRed, setChangeIndexForRed] = useState<Array<number | null>>([]);

  const changeColor = (id: number) => {
    return changeIndexForGreen.includes(id)
      ? 'green'
      : changeIndexForRed.includes(id)
        ? 'red'
        : 'bisque'
  };

  const Item = ({ id }: { id: number }) => {
    return (
      <View
        key={id}
        style={[
          styles.view,
          {
            backgroundColor: changeColor(id)
          }
        ]}
      >
        <Text style={{ color: 'black', fontSize: 30 }}>{id}</Text>
      </View>
    )
  };

  const getItem = (data: ItemDataProp[], index: number) => data[index];
  const getItemCount = (data: ItemDataProp[]) => data.length;

  const viewabilityConfigForGreen = {
    minimumViewTime: 500,
    //viewAreaCoveragePercentThreshold: 80,
    itemVisiblePercentThreshold: 100,
    waitForInteraction: false
  };

  const onViewableItemsChangedForGreen = ((info: InfoProp) => {
    const { viewableItems, changed } = info;
    //console.log("Viewable Items: ", viewableItems);
    //console.log("Changed Items: ", changed);
    const indexArray = viewableItems.map((item: ViewToken) => item.index)
    setChangeIndexForGreen(indexArray);
  });

  const viewabilityConfigForRed = {
    minimumViewTime: 500,
    itemVisiblePercentThreshold: 10,
    waitForInteraction: false
  };

  const onViewableItemsChangedForRed = (info: InfoProp) => {
    const { viewableItems, changed } = info;
    const indexArray = viewableItems.map((item: ViewToken) => item.index)
    setChangeIndexForRed(indexArray);
  };

  const viewabilityConfigCallbackPairs = useRef([
    {
      viewabilityConfig: viewabilityConfigForGreen,
      onViewableItemsChanged: onViewableItemsChangedForGreen
    },
    {
      viewabilityConfig: viewabilityConfigForRed,
      onViewableItemsChanged: onViewableItemsChangedForRed
    },
  ]);

  const viewabilityConfigCallbackPairsForOnePair = useRef([
    {
      viewabilityConfig: viewabilityConfigForGreen,
      onViewableItemsChanged: onViewableItemsChangedForGreen
    }
  ]);

  return (
    <SafeAreaView style={{ flex: 1, backgroundColor: 'white' }}>
      <StatusBar
        barStyle="dark-content"
      />
      <View style={styles.container}>
        <Text style={styles.text}>
          ifelsething.com
        </Text>
        <Text style={styles.text}>
          viewable items in react native
        </Text>
        <View style={{ height: '100%' }}>
          <VirtualizedList
            contentContainerStyle={styles.content_container}
            data={listData}
            getItemCount={getItemCount}
            getItem={getItem}
            // viewabilityConfig={viewabilityConfigForGreen.current}
            // onViewableItemsChanged={onViewableItemsChangedForGreen}
            viewabilityConfigCallbackPairs={viewabilityConfigCallbackPairs.current}
            // viewabilityConfigCallbackPairs={viewabilityConfigCallbackPairsForOnePair.current}
            keyExtractor={item => item.id + ""}
            renderItem={({ index }) => {
              return (
                <Item id={index} />
              )
            }}
          />
        </View>
      </View>
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    margin: 10,
    gap: 20
  },
  text: {
    fontSize: 15,
    color: 'black',
    fontStyle: 'italic'
  },
  content_container: {
    gap: 10
  },
  view: {
    justifyContent: 'center',
    alignItems: 'center',
    width: '100%',
    borderRadius: 10,
    height: 300
  }
});

Share is Caring

Related Posts