FlatList scrollToIndex out of range

Tags: , , , ,



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;

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.

  1. 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.

  2. 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.

  3. 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.

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.



Source: stackoverflow