Reputation: 472
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;
Upvotes: 8
Views: 20859
Reputation: 37318
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 the App
component.
You are passing item as data
to the AdSlider
and you are trying to fetch that as the data.length
, which is obvious that it's not going to work because the data
is the item
which is an object and not an array.
You don't need to use the effects inside the AdSlider
, set just one effect inside the App
and change currentIndex
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 calling scrollToIndex
for forcing the list to update and re-render.
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]);
...
}
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.
Upvotes: 7
Reputation: 81
looks like your if
statement is incorrect, the maximum index should be totalLength - 1
.
for example, we have an array of 3 items: [{id: 1, index: 0}, {id: 2, index: 1}, {id: 3, index: 2}]
, then the length of the array is 3
, but the maximum index is 2
, so when the current index is ">= 2 (totalLength - 1)", you should reset it to 0
. and for the else conditions, set next index to 'currentIdx + 1'
if(activeIdx === ITEMS.length - 1){
setActiveIdx(0)
} else {
setActiveIdx(idx => idx + 1);
}
for more detailed, code may look like this:
function Slider(props) {
const ref = React.useRef(null);
const [activeIdx, setActiveIdx] = React.useState(0)
React.useEffect(() => {
ref.current.scroll({left: ITEM_WIDTH * activeIdx, behavior: "smooth"}) // please use .scrollToIndex here
}, [activeIdx]);
React.useEffect(() => {
let timer = setTimeout(() => {
let nextIdx = activeIdx === ITEMS.length - 1 ? 0 : activeIdx + 1;
setActiveIdx(nextIdx)
}, 3000);
return () => clearTimeout(timer)
}, [activeIdx]);
return (...)
}
Upvotes: 3