43

onViewableItemsChanged does not seem to work when there is a state change in the app. Is this correct?

Seems like it wouldn't be very useful if this were the case....

Otherwise, users will be forced to us onScroll in order to determine position or something similar...

Steps to Reproduce

  1. Please refer to snack
  2. Repo has also been uploaded at github
  3. Any state change produces an error when using onViewableItemsChanged
  4. What does this error even mean?

Note: Placing the onViewableItemsChanged function in a const outside the render method also does not assist...

<FlatList
    data={this.state.cardData}
    horizontal={true}
    pagingEnabled={true}
    showsHorizontalScrollIndicator={false}
    onViewableItemsChanged={(info) =>console.log(info)}
    viewabilityConfig={{viewAreaCoveragePercentThreshold: 50}}
    renderItem={({item}) =>
        <View style={{width: width, borderColor: 'white', borderWidth: 20,}}>
            <Text>Dogs and Cats</Text>
        </View>
    }
/>

Actual Behavior

Error

image

Rafael Tavares
  • 5,678
  • 4
  • 32
  • 48
njho
  • 2,023
  • 4
  • 22
  • 37

11 Answers11

107

Based on @woodpav comment. Using functional components and Hooks. Assign both viewabilityConfig to a ref and onViewableItemsChanged to a useCallback to ensure the identities are stable and use those. Something like below:

  const onViewCallBack = React.useCallback((viewableItems)=> {
      console.log(viewableItems)
      // Use viewable items in state or as intended
  }, []) // any dependencies that require the function to be "redeclared"

  const viewConfigRef = React.useRef({ viewAreaCoveragePercentThreshold: 50 })


<FlatList
      horizontal={true}
      onViewableItemsChanged={onViewCallBack}
      data={Object.keys(cards)}
      keyExtractor={(_, index) => index.toString()}
      viewabilityConfig={viewConfigRef.current}
      renderItem={({ item, index }) => { ... }}
/>
Stuart Gough
  • 1,564
  • 3
  • 11
  • 17
  • 8
    This doesn't work if you try to access a state property inside the method since it's not updated due to the nature of ref – mxmtsk Jun 06 '20 at 17:39
  • 1
    @MaxTommyMitschke The ref is just a ref to the function so you are correct in that there will be no re-render there. However, you can set state with the `useState` (ie `setViewableItems(viewableItems)`) hook and set it within the callback. I haven't encountered any stale state here before. The scope of the ref is still within the functional component so scoping should not be a problem either. – Stuart Gough Jun 07 '20 at 15:49
  • @StuartGough can you please explain how can I use setState within the useRef. I am getting error: "can't perform a react state update on an unmounted component". – Vipul Feb 02 '22 at 16:11
  • 1
    @Vipul the reason why you would get that is because a state update would cause a re-render of the component. Its really a question of ensuring "all" identities in memory are stable. So assigning onViewRef and viewConfigRef have solved that portion. Wrapping the FlatList in its own component and memoizing that. Then you can pass the setState as a prop. Keep in mind you should not be setting the content on the fly from that (otherwise the re-render will happen anyway). In essence stable identies with a bit of encapsulation ensure the "same" FlatList is mounted and that will solve your issue. – Stuart Gough Feb 02 '22 at 16:46
  • @StuartGough Is there any way possible to do this without creating a separate component for Flatlist? – Vipul Feb 02 '22 at 18:47
26

The error "Changing onViewableItemsChanged on the fly is not supported" occurs because when you update the state, you are creating a new onViewableItemsChanged function reference, so you are changing it on the fly.

While the other answer may solve the issue with useRef, it is not the correct hook in this case. You should be using useCallback to return a memoized callback and useState to get the current state without needing to create a new reference to the function.

Here is an example that save all viewed items index on state:

const MyComp = () => {
  const [cardData] = useState(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i']);
  const [viewedItems, setViewedItems] = useState([]);

  const handleVieweableItemsChanged = useCallback(({ changed }) => {
    setViewedItems(oldViewedItems => {
      // We can have access to the current state without adding it
      //  to the useCallback dependencies

      let newViewedItems = null;

      changed.forEach(({ index, isViewable }) => {
        if (index != null && isViewable && !oldViewedItems.includes(index)) {
          
           if (newViewedItems == null) {
             newViewedItems = [...oldViewedItems];
           }
           newViewedItems.push(index);
        }
      });

      // If the items didn't change, we return the old items so
      //  an unnecessary re-render is avoided.
      return newViewedItems == null ? oldViewedItems : newViewedItems;
    });

    // Since it has no dependencies, this function is created only once
  }, []);

  function renderItem({ index, item }) {
    const viewed = '' + viewedItems.includes(index);
    return (
      <View>
        <Text>Data: {item}, Viewed: {viewed}</Text>
      </View>
    );
  }

  return (
    <FlatList
      data={cardData}
      onViewableItemsChanged={handleVieweableItemsChanged}
      viewabilityConfig={this.viewabilityConfig}
      renderItem={renderItem}
    />
  );
}

You can see it working on Snack.

Rafael Tavares
  • 5,678
  • 4
  • 32
  • 48
  • something to note: if the final component being returned is not a FlatList, but a wrapper that takes the FlatList props (`onViewableItemsChanged`, `viewabilityConfig`) to later be used in a FlatList elsewhere, trying to update state within `onViewableItemsChanged` will throw an error (cannot update from outside). The solution for me was to leave `handleVieweableItemsChanged` inside of a `useCallback` with 0 deps, and whatever the deps actually are, set as `useRef` variables. So in the above example `viewedItems = useRef([])` and then `viewedItems.current` within `handleVieweableItemsChanged ` – sweatherall Jan 11 '21 at 19:45
  • I don't know for some reason, previously it was working fine, but now it started throwing error. So I had to rely back to the same `useRef` thing to solve this out. So the accepted answer worked out for me. – Alok Oct 04 '21 at 13:36
  • 2
    Quick heads up: with this approach you loose the power of hot reload, because the function will be different between reloads. – Anderson Madeira Nov 10 '21 at 13:06
6

In 2023 with react-native version 0.71.2, the following code seems to work better than the older answers.


// 1. Define a function outside the component: 
const onViewableItemsChanged = (info) => {
  console.log(info);
};

// 2. create a reference to the function (above)
const viewabilityConfigCallbackPairs = useRef([
  { onViewableItemsChanged },
]);

<FlatList
    data={this.state.cardData}
    horizontal={true}
    pagingEnabled={true}
    showsHorizontalScrollIndicator={false}    
    viewabilityConfig={{viewAreaCoveragePercentThreshold: 50}}
    
    // remove the following statement 
    // onViewableItemsChanged={(info) =>console.log(info)}
    
    // 3. add the following statement, instead of the one above
    viewabilityConfigCallbackPairs={viewabilityConfigCallbackPairs.current}

    renderItem={({item}) =>
        <View style={{width: width, borderColor: 'white', borderWidth: 20,}}>
            <Text>Dogs and Cats</Text>
        </View>
    }
/>

Source: https://github.com/facebook/react-native/issues/30171#issuecomment-820833606

Bilal Abdeen
  • 1,627
  • 1
  • 19
  • 41
3

You must pass in a function to onViewableItemsChanged that is bound in the constructor of the component and you must set viewabilityConfig as a constant outside of the Flatlist.

Example:

class YourComponent extends Component {

    constructor() {
        super()
        this.onViewableItemsChanged.bind(this)
    }

    onViewableItemsChanged({viewableItems, changed}) {
        console.log('viewableItems', viewableItems)
        console.log('changed', changed)
    }

    viewabilityConfig = {viewAreaCoveragePercentThreshold: 50}

    render() {
        return(
          <FlatList
            data={this.state.cardData}
            horizontal={true}
            pagingEnabled={true}
            showsHorizontalScrollIndicator={false}
            onViewableItemsChanged={this.onViewableItemsChanged}
            viewabilityConfig={this.viewabilityConfig}
            renderItem={({item}) =>
                <View style={{width: width, borderColor: 'white', borderWidth: 20,}}>
                    <Text>Dogs and Cats</Text>
                 </View>}
          />
        )
    }
}
Rishav
  • 3,818
  • 1
  • 31
  • 49
C.Kelly
  • 215
  • 1
  • 12
  • 2
    If you want to do this with hooks use the useRef hook and set the initial value to your viewability config. Access it later with ref.current. useRef creates the functional equivalent of a `this` variable. – woodpav Mar 27 '19 at 22:13
  • Hi @woodpav I didnt manage to work with hooks ı am still getting the same error. If you explain or show working code, ıt will be very good – Kaan Öner Jun 20 '19 at 11:09
  • 4
    Write a `viewabilityConfig = useRef({...})` and a `onViewableItemsChanged = useRef(() => {...})` hook. Set the corresponding `FlatList` prop to `viewabilityConfig.current` and `onViewableItemsChanged.current`. Don't alter these or you will experience the error. – woodpav Jun 20 '19 at 22:12
  • @woodpav you should probably post this as a separate answer, cause hooks are great and this is the single place where this issue addressed with hooks :) – Nikita Neganov Jul 29 '19 at 17:29
0
const handleItemChange = useCallback( ({viewableItems}) => {
console.log('here are the chaneges', viewableItems);
if(viewableItems.length>=1)
viewableItems[0].isViewable?
  setChange(viewableItems[0].index):null;

},[])

try this one it work for me

Pravin Ghorle
  • 606
  • 7
  • 7
0

Setting both onViewableItemsChanged and viewabilityConfig outside the flatlist solved my problem.

const onViewableItemsChanged = useCallback(({ viewableItems }) => {
    if (viewableItems.length >= 1) {
      if (viewableItems[0].isViewable) {
        setItem(items[viewableItems[0].index]);
        setActiveIndex(viewableItems[0].index);
      }
    }
  }, []);



const viewabilityConfig = {
    viewAreaCoveragePercentThreshold: 50,
  };

I'm using functional component and my flatlist looks like this

  <Animated.FlatList
     data={items}
     keyExtractor={item => item.key}
     horizontal
     initialScrollIndex={activeIndex}
     pagingEnabled
     onViewableItemsChanged={onViewableItemsChanged}
     viewabilityConfig={viewabilityConfig}
     ref={flatlistRef}
     onScroll={Animated.event(
       [{ nativeEvent: { contentOffset: { x: scrollX } } }],
       { useNativeDriver: false },
     )}
     contentContainerStyle={{
       paddingBottom: 10,
     }}
     showsHorizontalScrollIndicator={false}
     renderItem={({ item }) => {
       return (
         <View style={{ width, alignItems: 'center' }}>
           <SomeComponent item={item} />
         </View>
       );
     }}
   />
Fabi Mendes
  • 131
  • 1
  • 3
0

Try using viewabilityConfigCallbackPairs instead of onViewableItemsChanged.

import React, {useRef} from 'react';

const App = () => {
    // The name of the function must be onViewableItemsChanged.
    const onViewableItemsChanged = ({viewableItems}) => {
        console.log(viewableItems);
        // Your code here.
    };
    const viewabilityConfigCallbackPairs = useRef([{onViewableItemsChanged}]);

    return (
        <View style={styles.root}>
            <FlatList
              data={data}
              renderItem={renderItem}
              keyExtractor={item => item.id}
              viewabilityConfigCallbackPairs={
                  viewabilityConfigCallbackPairs.current
              }
            />
        </View>
    );
}
Manil Malla
  • 133
  • 8
-1

Move the viewabilityConfig object to the constructor.

constructor() {
    this.viewabilityConfig = {
        viewAreaCoveragePercentThreshold: 50
    };
}

render() {
     return(
        <FlatList
            data={this.state.cardData}
            horizontal={true}
            pagingEnabled={true}
            showsHorizontalScrollIndicator={false}
            onViewableItemsChanged={(info) =>console.log(info)}
            viewabilityConfig={this.viewabilityConfig}
            renderItem={({item}) =>
                <View style={{width: width, borderColor: 'white', borderWidth: 20,}}>
                    <Text>Dogs and Cats</Text>
                </View>
            }
        />
    )
}
Dagobert Renouf
  • 2,078
  • 2
  • 14
  • 14
-1

Sombody suggest to use extraData property of Flatlist to let Flatlist notice, that something changed.

But this didn't work for me, here is what work for me:

Use key={this.state.orientation} while orientation e.g is "portrait" or "landscape"... it can be everything you want, but it had to change, if the orientation changed. If Flatlist notice that the key-property is changed, it rerenders.

works for react-native 0.56

suther
  • 12,600
  • 4
  • 62
  • 99
-1

this works for me, is there any way to pass an additional argument to onViewRef? Like in the below code how can i pass type argument to onViewRef. Code:

        function getScrollItems(items, isPendingList, type) {
    return (
        <FlatList
            data={items}
            style={{width: wp("100%"), paddingLeft: wp("4%"), paddingRight: wp("10%")}}
            horizontal={true}
            keyExtractor={(item, index) => index.toString()}
            showsHorizontalScrollIndicator={false}
            renderItem={({item, index}) => renderScrollItem(item, index, isPendingList, type)}
            viewabilityConfig={viewConfigRef.current}
            onViewableItemsChanged={onViewRef.current}
        />
    )
}
Ashish Guleria
  • 277
  • 1
  • 5
-2

Remove your viewabilityConfig prop to a const value outside the render functions as well as your onViewableItemsChanged function

AndrewK
  • 194
  • 2
  • 9