FlatList scrollToIndex вне диапазона

#javascript #reactjs #react-native #react-hooks #use-state

#javascript #reactjs #react-native #реагирующие крючки #состояние использования

Вопрос:

У меня есть плоский список, в котором я пытаюсь прокручивать каждый индекс моего массива данных каждые X секунд. В моем массиве сейчас только два элемента, но их может быть больше. Текущий код работает для первых двух итераций, но затем, похоже, он не сбрасывается должным образом, и я получаю scrollToIndex out of range error: index is 2 but maximum is 1 . Я бы подумал, что когда currentIndex is >= data.length my if statement setCurrentIndex вернется к 0, но, похоже, это не работает. В основном то, что я пытаюсь сделать, это автоматически перебирать элементы в Flatlist, но каждый элемент останавливается на несколько секунд.

 /**
 * 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;

 

Комментарии:

1. Первые 2 строки, похоже, не принадлежат компоненту. useState и useRef должны использоваться в начале компонента.

2. Я обновил сообщение, чтобы отразить полный код

Ответ №1:

Вы получаете эту ошибку, потому что вы передаете item as data AdSlider компоненту, и у него, конечно, нет никакого length свойства, поэтому он возвращает undefined for data.length , и это не вычисляет выражение currentIndex === data.length - 1 , которым оно становится currentIndex === undefined - 1 , таким образом currentIndex , будет увеличиваться 1 без остановки, и оно достигнет значения 2 , которое выходит за рамки.

В вашем коде есть несколько проблем.

  1. У вас не должно быть компонента внутри другого компонента, особенно при использовании эффектов и состояний из родительского компонента. Удалить AdSlider за пределы App компонента.
  2. Вы передаете элемент в качестве data AdSlider и пытаетесь получить его как the data.length , что очевидно, что это не сработает, потому data item что это объект, а не массив.
  3. Вам не нужно использовать эффекты внутри AdSlider , установите только один эффект внутри App и измените currentIndex его на ref вместо переменной состояния, потому что вам не нужно, чтобы он изменял состояние для повторного отображения, потому что вы вызываете scrollToIndex принудительное обновление и повторное отображение списка.

Заставить его работать с использованием состояния и setTimeout

Если вы хотите, чтобы код работал с currentIndex состоянием in (что вам не нужно), вы можете перемещать эффекты внутри App компонента и изменять data.length с ads.length помощью, и это будет работать.

 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

Лучшее, что можно сделать, это преобразовать currentIndex в быть ссылкой и использовать setInterval вместо setTimeout вызова таймера цикла каждые 5 секунд:

 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);
  }, []);

  ...
}
 

Вы можете проверить рабочую закуску Expo здесь.

Комментарии:

1. Ну, кажется, это работает, но приложение ненадолго выходит из строя, а затем выдает следующую ошибку: null is not an object(evaluating 'myRef.current.scrollToIndex') . Я обновил свой пост, чтобы отразить новые изменения.

2. Я собираюсь отметить ваш ответ как правильный. Я думаю panResponder , что введенный мной модальный код вызывает еще одну проблему с ошибкой, которую я получаю сейчас. Спасибо!

3. Спасибо за щедрость. Ошибка null is not an object(evaluating 'myRef.current.scrollToIndex') , скорее всего, происходит из-за быстрого обновления ( или горячей перезагрузки ), и эффект срабатывает до myRef получения фактической ссылки на список, поэтому при первом вызове после повторного запроса он равен null. Вы либо проверяете myRef.current , не является ли это ложным, либо даже лучше использовать необязательную цепочку для вызова scrollToIndex подобным myRef?.current?.scrollToIndex(...) образом.

Ответ №2:

похоже, ваше if утверждение неверно, максимальный индекс должен быть totalLength - 1 . например, у нас есть массив из 3 элементов: [{id: 1, index: 0}, {id: 2, index: 1}, {id: 3, index: 2}] , тогда длина массива равна 3 , но максимальный индекс равен 2 , поэтому, когда текущий индекс равен «> = 2 (totalLength — 1)», вы должны сбросить его на 0 . и для условий else установите следующий индекс в ‘currentIdx 1’

       if(activeIdx === ITEMS.length - 1){
        setActiveIdx(0)
      } else {
        setActiveIdx(idx => idx   1);
      }
 

для получения более подробной информации код может выглядеть следующим образом:

 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 (...)
}
 

Комментарии:

1. Я реализовал ваше предложение, но я все равно получаю scrollToIndex out of range: requested index 2 but maximum is 1 . Я обновил сообщение новым кодом