FlatList ScrollView Error on any State Change - Invariant Violation: Changing onViewableItemsChanged on the fly is not supported
Based on @woodpav comment. Using functional components and Hooks.
Assign both viewabilityConfig
and onViewableItemsChanged
to refs and use those. Something like below:
const onViewRef = React.useRef((viewableItems)=> {
console.log(viewableItems)
// Use viewable items in state or as intended
})
const viewConfigRef = React.useRef({ viewAreaCoveragePercentThreshold: 50 })
<FlatList
horizontal={true}
onViewableItemsChanged={onViewRef.current}
data={Object.keys(cards)}
keyExtractor={(_, index) => index.toString()}
viewabilityConfig={viewConfigRef.current}
renderItem={({ item, index }) => { ... }}
/>
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 accepted 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.
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>}
/>
)
}
}