#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
, которое выходит за рамки.
В вашем коде есть несколько проблем.
- У вас не должно быть компонента внутри другого компонента, особенно при использовании эффектов и состояний из родительского компонента. Удалить
AdSlider
за пределыApp
компонента. - Вы передаете элемент в качестве
data
AdSlider
и пытаетесь получить его как thedata.length
, что очевидно, что это не сработает, потомуdata
item
что это объект, а не массив. - Вам не нужно использовать эффекты внутри
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
. Я обновил сообщение новым кодом