I have a FlatList where I’m trying to scroll through each index of my data array every X amount of seconds. There’s only two items in my array right now but there could be more. The current code works for the first two iterations but then it does not seem to reset properly and I get the scrollToIndex out of range error: index is 2 but maximum is 1
. I would think that when the currentIndex
is >= data.length
my if
statement would setCurrentIndex
back to 0 but it doesn’t seem to work. Basically what I’m trying to do is loop the items in the Flatlist automatically but each item pausing for a few seconds.
/** * Sample React Native App * https://github.com/facebook/react-native * * @format * @flow strict-local */ import 'react-native-gesture-handler'; import React, {useState, useEffect, useRef} from 'react'; import { NavigationContainer } from '@react-navigation/native'; import { createStackNavigator, HeaderBackButton } from '@react-navigation/stack'; import { SafeAreaView, StyleSheet, ScrollView, View, Text, StatusBar, ImageBackground, Image, TextInput, Button, TouchableNativeFeedback, TouchableWithoutFeedback, TouchableOpacity, Modal, Pressable, PanResponder, FlatList, Dimensions } from 'react-native'; import { Immersive } from 'react-native-immersive'; import { Header, LearnMoreLinks, Colors, DebugInstructions, ReloadInstructions, } from 'react-native/Libraries/NewAppScreen'; import WineList from './screens/WineList'; import Home from './screens/Home'; import Rate from './screens/Rate'; import Thankyou from './screens/Thankyou'; const Stack = createStackNavigator(); const { width: windowWidth, height: windowHeight } = Dimensions.get("window"); const wineclub = require('./images/wineclub.png'); const gaspers = require('./images/gaspers.png'); const qrcode = require('./images/wineclubQR.png'); let ads = [ { adImg: wineclub, adTitle: 'Space will be limited so join online today!', adInfo: ' Upon joining, both clubs will be billed our Trio Pre-Opening Promotion', qrCodeImg: qrcode }, { adImg: gaspers, adTitle: 'Coming Soon!', adInfo: 'Gourmet chef designed menu. Stunning views. Modern romantic decor', qrCodeImg: qrcode } ] function AdSlider({data}){ return( <View style={{alignContent:'center', alignItems:'center', backgroundColor:'#4B4239', height:1400}}> <Image source={data.adImg} style={{width:640,height:500}} ></Image> <Text style={{color:'white', fontFamily:'LaoMN', fontSize:30, marginTop:20}}>{data.adTitle}</Text> <Text style={{color:'white', fontFamily:'LaoMN', fontSize:20, marginTop:20, textAlign:'center'}} > {data.adInfo} </Text> <View style={{flexDirection:'row', justifyContent:'flex-start', alignContent:'center', alignItems:'center', marginTop:20}}> <Text style={{fontSize:40, color:'white', padding:20}}>Scan Here </Text> <Image source={data.qrCodeImg}></Image> </View> </View> ) } const App: () => React$Node = () => { Immersive.on() Immersive.setImmersive(true) const navigationRef = useRef(null); const myRef = useRef(null); const currentIndex = useRef(0); const [modalVisible, setModalVisible] = useState(false); const timerId = useRef(false); const [timeForInactivityInSecond, setTimeForInactivityInSecond] = useState( 5 ) useEffect(() => { resetInactivityTimeout() },[]) const panResponder = React.useRef( PanResponder.create({ onStartShouldSetPanResponderCapture: () => { // console.log('user starts touch'); setModalVisible(false) resetInactivityTimeout() }, }) ).current const resetInactivityTimeout = () => { clearTimeout(timerId.current) timerId.current = setTimeout(() => { // action after user has been detected idle setModalVisible(true) navigationRef.current?.navigate('Home'); }, timeForInactivityInSecond * 1000) } // for the slider useEffect(() => { const timer = setInterval(() => { currentIndex.current = currentIndex.current === ads.length - 1 ? 0 : currentIndex.current + 1; myRef.current.scrollToIndex({ animated: true, index: currentIndex.current , }); }, 5000); return () => clearInterval(timer); }, []); return ( <NavigationContainer ref={navigationRef} > <View {...panResponder.panHandlers} style={{ flex:1}}> <TouchableWithoutFeedback > <Modal animationType="slide" transparent={false} hardwareAccelerated={false} visible={modalVisible} > <FlatList ref={myRef} data={ads} renderItem={({ item, index }) => { return <AdSlider key={index} data={item} dataLength={ads.length} />; }} pagingEnabled horizontal showsHorizontalScrollIndicator={false} /> </Modal> </TouchableWithoutFeedback> <Stack.Navigator navigationOptions={{headerTintColor: '#ffffff',}} screenOptions={{ headerTintColor: '#ffffff', cardStyle: { backgroundColor: '#4B4239' }, }} > <Stack.Screen name="Home" component={Home} options={{ headerShown: false, }} /> <Stack.Screen name="WineList" component={WineList} options={{ title: 'Exit', headerStyle: { backgroundColor: '#4B4239', }, headerTintColor: '#fff', headerTitleStyle: { fontWeight: 'bold', }, }}/> <Stack.Screen name="Rate" component={Rate} options={{ title: 'Back to Selections', headerStyle: { backgroundColor: '#4B4239', }, headerTintColor: '#fff', headerTitleStyle: { fontWeight: 'bold', }, }}/> <Stack.Screen name="Thankyou" component={Thankyou} options={ { headerShown: false, title: 'Home', headerStyle: { backgroundColor: '#4B4239', }, headerTintColor: '#fff', headerTitleStyle: { fontWeight: 'bold', }, }}/> </Stack.Navigator> </View> </NavigationContainer> ); }; export default App;
Advertisement
Answer
You’re getting this error because you are passing the item
as data
to the AdSlider
component and it does not have any length
property of course thus it returns undefined
for data.length
and that does not evaluate the expression currentIndex === data.length - 1
which it becomes currentIndex === undefined - 1
thus currentIndex
will get increased by 1
without stopping and it will reach the value of 2
which is out of bounds.
There are several issues with your code.
You should not have a component inside another component and especially not when using effects and state from the parent component. Remove
AdSlider
outside of theApp
component.You are passing item as
data
to theAdSlider
and you are trying to fetch that as thedata.length
, which is obvious that it’s not going to work because thedata
is theitem
which is an object and not an array.You don’t need to use the effects inside the
AdSlider
, set just one effect inside theApp
and changecurrentIndex
to be a ref instead of a state variable because you don’t need it’s changing state in order to re-render because you’re callingscrollToIndex
for forcing the list to update and re-render.
Making it work using state and setTimeout
If you want to make the code wotk with currentIndex
being in state (which you don’t need), you can move effects inside the App
component and change data.length
with ads.length
and it will work.
const App: () => React$Node = () => { Immersive.on() Immersive.setImmersive(true) const navigationRef = useRef(null); const myRef = useRef(null); const [currentIndex, setCurrentIndex] = useState(0); useEffect(() => { myRef.current.scrollToIndex({ animated: true, index: currentIndex , }); }, [currentIndex]); useEffect(()=> { const timer = setTimeout(()=> { // Change data.length to ads.length here const nextIndex = currentIndex === ads.length - 1 ? 0 : currentIndex + 1; setCurrentIndex(nextIndex); }, 5000); return () => clearTimeout(timer); }, [currentIndex]); ... }
Making it work using ref and setInterval
Best thing to do though, is to convert currentIndex
to a be a ref and use setInterval
instead of setTimeout
to have a looping timer call every 5 seconds:
const App: () => React$Node = () => { Immersive.on() Immersive.setImmersive(true) const navigationRef = useRef(null); const myRef = useRef(null); // Make currentIndex a ref instead of a state variable, // because we don't need the re-renders // nor to trigger any effects depending on it const currentIndex = useRef(0); useEffect(() => { // Have a timer call the function every 5 seconds using setInterval const timer = setInterval(() => { // Change data.length to ads.length here currentIndex.current = currentIndex.current === ads.length - 1 ? 0 : currentIndex.current + 1; myRef.current.scrollToIndex({ animated: true, index: currentIndex.current, }); }, 5000); return () => clearInterval(timer); }, []); ... }
You can check a working Expo Snack here.