ScrollView positions being restarted on a child component re-render (update) - reactjs

I am having an issue (this is probably something I am ignoring within the render cycle) maintaining the position of two different scroll views when some of its child components are updated.
I have a hierarchy of views that is like:
<ScrollView vertical scrolling ability>
...
<ScrollView horizontal and vertical scrolling ability>
...
<Matrix>
<Cell updateCellStatusHandler={handler}>
</Matrix>
...
</ScrollView>
</ScrollView>
So, the updates on the internal cells, are resetting both scrolls on cell status update and this generates a super weird experience with the user having to scroll down/left/right back to continue interacting with the Matrix of cells with a status I have.
I have tried to save the scrollOffset (x,y) using useState but if I change some cell, the state is reseted to (0,0) which is my initial state.
const [scrollOffset, setScrollOffset] = useState({
scrollX: 0,
scrollY: 0,
})
But without luck.
<ScrollView
{...props}
scrollEventThrottle={16}
ref={scrollReference}
// tslint:disable-next-line: jsx-no-lambda
onScroll={event => {
setScrollOffset({
scrollX: event.nativeEvent.contentOffset.x,
scrollY: event.nativeEvent.contentOffset.y,
})
}}
onScrollEndDrag={event => {
console.log(event.nativeEvent.contentOffset.y)
console.log(event.nativeEvent.contentOffset.x)
}}
>
{props.children}
</ScrollView>
One possible approach to solve this is to have a mechanism that allow me to save the scroll position before the update. But this will complicate a lot the communication between components, etc.
By the way, the cell status update is being handled via Redux.
If some of you can bring you some light over this, would be great.
---- UPDATE 1 (Code of the panel component added) ----
Parent component is:
<View style={styles.container}>
<CustomScrollView enableResetScrollToCoords={false}>
<Section>
<WhiteContainer>
<View style={styles.rateContainer}>
<DescriptionTextStatus
statusText={getMessageForRate(availabilityRate)}
descriptionText={getDescriptionForRate(
availabilityRate,
isTeacher,
)}
icon={getImageForRate(availabilityRate)}
/>
</View>
</WhiteContainer>
</Section>
{!loggedUserIsTeacher() && <AvailabilityStart />}
<AvailabilityPanel />
<AvailabilityStatus />
<AvailabilityButtonSave />
</CustomScrollView>
</View>
Availability Panel is one of the childs
export const AvailabilityPanel: React.FunctionComponent<{}> = () => {
const panel: Cell[][] = useSelector((state: ReduxStore) => {
return get(state.entities, 'availability.panel', undefined)
})
if (panel === undefined) {
return <Nothing />
}
return (
<Section>
<WhiteContainer>
<LinearGradient
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
colors={[Palette.White, Palette.White68]}
>
<View style={styles.rateContainer}>
<DescriptionTextStatus
statusText={strings.warning}
descriptionText={strings.selectionMessage}
icon={'clock'}
/>
<View style={styles.separator} />
</View>
<View style={styles.container}>
<ScrollView
style={styles.scrollView}
directionalLockEnabled={false}
horizontal={true}
showsHorizontalScrollIndicator={false}
showsVerticalScrollIndicator={false}
>
<View style={styles.contentContainer}>
<View style={styles.weekdaysContainer}>
<PanelHeader weekdays={weekdays} />
</View>
<View style={styles.rowsContainer}>
{hours.map((hourLabel: string, index: number) => {
return (
<PanelRow
key={index}
hourLabel={hourLabel}
hoursRow={panel[index]}
rowIndex={index}
/>
)
})}
</View>
</View>
</ScrollView>
</View>
</LinearGradient>
</WhiteContainer>
</Section>
)
}
Thanks in advance.

ScrollView does not reset scroll position because of child updates, regardless of whether you use Redux or not. You don't need to try to patch it with workarounds like saving current scroll positions. Rather just figure out what's wrong with your setup
It's hard to tell because you didn't provide your actual code, but I'm pretty sure one or both ScrollViews are remounted on updates (old ScrollView is removed and new one is created in its place with its own scroll position). It's likely that you are declaring new components inside render function so React will just remount them every time

I think the overall idea of saving the scroll position of your scrollview is good.
I think the fact that your state is reset to 0 is probably due to Redux (not sure however, could you precise how your component is connected to redux ?). I am not so familiar with Hooks, but in a React Class component, I would try to save the scroll position in a non state property of the class. Maybe save it in a variable outside of your functional component ?
Then in the handler to manage cell updates, you could ensure that your scrollview scrolls to the position you saved (using ScrollView.scrollTo(), with {animated: false} option so that the animation is not visible for the user)

Related

React Native FlatList: how to reset scroll position

I have a horizontal FlatList in my React Native iOS app. At the bottom of the screen, the FlatList is displayed with 5 items. If you click on the last item (scrolled all the way to the right), the screen will refresh and the FlatList will remain scrolled to the right. How can I get the FlatList to always reset the scroll position (to scroll from the beginning) when the screen changes?
Edit: I have a feeling that my screen is not actually "refreshing" but rather merely changing the data shown on the screen. In this case I may need to trigger a refresh of the screen somehow to cause the FlatList to reset the scroll position? Any help would be greatly appreciated!
const HorizontalScroll = ({items, handlePress}) => {
const renderItem = ({item}) => {
const itemData = { color: item.color, title: item.title };
return <HorizontalItem itemData={itemData} handlePress={handlePress} />;
};
return (
<View style={styles.container}>
<FlatList
data={items}
renderItem={renderItem}
horizontal={true}
numColumns={1}
key={(item) => item.id}
initialNumToRender={5}
scrollToIndex={0}
ItemSeparatorComponent={() => <View style={{width: 8}} />}>
</FlatList>
</View>
);
};
I solved this problem by forcing a refresh of the component. Specifically, I added a key prop to my component so that every time the key value changes, it remounts the component. This causes the FlatList to reset the scroll position to the beginning.
<View key={items[0].title}>
{content}
</View>

Changing content of a component with each swipe of Swiper Library

I am new to React Native and am not completely familiar with state management. Currently I am using react-native-swiper library to hold my swipable images. Each swipe takes me to the next image. Code below
Fish.js:
<Swiper style={styles.wrapper} height={10} horizontal={true} >
<View style={styles.slide1}>
<Image
resizeMode="stretch"
style={styles.image}
source={require('../resource/images/AzureDamsel.png')}
/>
</View>
<View style={styles.slide2}>
<Image
resizeMode="stretch"
style={styles.image}
source={require('../resource/images/BicolourAngelfish.png')}
/>
</View>
<View style={styles.slide3}>
<Image
resizeMode="stretch"
style={styles.image}
source={require('../resource/images/ClownTriggerfish.png')}
/>
</View>
</Swiper>
And I am importing this in my App.js where I have some Text component below this like so:
<View style={{ flex: 1 }}>
<ImageBackground
style={globalStyles.backgroundImage}
source={require('./resource/images/bg1080.jpeg')}
>
<Fish />
<Text> Fish 1</Text>
<Text> Fish 2</Text>
</ImageBackground>
</View>
But I want the respective Text to show with each swipe. Example at image 1 the Text says Fish 1, then when I swipe the image and go to the second slide, the Text changes to Fish 2 and so on. Any help would be appreciated.
You can maintain the active slide index in state and then render that index.
Inside of App.js
const [activeIndex, setActiveIndex] = useState();
const onIndexChanged = (index) => setActiveIndex(index);
return (
<View>
<Fish
onIndexChanged={onIndexChanged}
/>
{ activeIndex && <Text>`Fish ${activeIndex}`</Text> }
</View>
)
Inside of Fish.js you need to pass the onIndexChanged prop into Swiper.
const Fish = ({ onIndexChanged }) => (
<Swiper onIndexChange={onIndexChanged}>
// Rest of your code.
</Swiper>
)
Checking your Snack, I can see some output but as you mentioned it's not what you are expecting.
Start the activeIndex state variable to 0.
const [activeIndex, setActiveIndex] = useState(0);
Pass setActiveIndex straight into Fish via the onIndexChanged prop. Now you will be able to remove the unnecessary onIndexChanged function from App.js
<Fish onIndexChanged={setActiveIndex} />
Change your string interpolation to just render the text from fishMapping. Because activeIndex is no longer undefined, you do not need the additional check (activeIndex &&)
<Text>{fishMapping[activeIndex]}</Text>
Here's an updated version of your Snack.
I went through the snack provided by you and the issue is in the Fish.js file.
Replace onIndexChange with onIndexChanged. The updated code:
const Fish = ({ onIndexChanged }) => (
<Swiper onIndexChanged={onIndexChanged}>
// Rest of your code.
</Swiper>
)
Link to updated snack : https://snack.expo.dev/BToSGo1z6

How do I shorten the pull to refresh distance (threshold) in React Native Refresh Control

I am trying to implement pull to refresh using Refresh Control in ScrollView. I need to modify the pulling distance/threshold of refresh control. I have checked for the related props to achieve it, but I can't any props related to this.
<ScrollView
contentContainerStyle={styles.subContainer}
bounces={true}
horizontal={false}
refreshControl={
<RefreshControl
refreshing={false}
tintColor={'transparent'}
onRefresh={() => {
onSwipeDown();
}}
/>
}
>
{props.children}
</ScrollView>
Please help me guys.
We can achieve this by onScroll event listener.
<ScrollView
contentContainerStyle={styles.subContainer}
bounces={true}
horizontal={false}
onScroll={onSwipeDown}
>
{props.children}
</ScrollView>
onSwipeDown method
const onSwipeDown = event => {
console.log(event.nativeEvent.contentOffset.y);
if (event.nativeEvent.contentOffset.y < 0) { // you can replace zero with needed threshold
onRefresh(); //your refresh function
}
};

ScrollView doesn't keep track of current position. When tapped it resets back to previous location

Steps of the problem:
1: Scroll down page from location x
2: Page Scrolls to location y
3: Release finger
4: User taps finger again to continue scrolling
5: Location on the ScrollView resets back to location x causing a jump back and prevents further scrolling
I've tried to look up information regarding react-native-modal and ScrollView to find the issue & can't find any occurrences of this
Library-in-use: react-native-modal
scrollOffset: 0
scrollViewRef
handleScrollTo = p => {
if (this.scrollViewRef) {
this.scrollViewRef.scrollTo(p);
}
};
<Modal
isVisible={this.state.visibleModal}
onSwipeComplete={() => this.setState({ visibleModal: false })}
swipeDirection="down"
scrollTo={this.handleScrollTo}
scrollOffset={this.state.scrollOffset}
scrollOffsetMax={400 - 300} // content height - ScrollView height
style={screenStyles.bottomModal}
useNativeDriver
>
{this._renderModalContent()}
</Modal>
// Render modal content (there's a view wrapping this)
<ScrollView
style ={{flex: 8, paddingLeft: '10%', paddingTop: '10%'}}
ref={ref => (this.scrollViewRef = ref)}
onScroll={event => {this.state.scrollOffset = event.nativeEvent.contentOffset.y}}
scrollEventThrottle={16}>
>
{Object.keys(balances).map(
symbol =>
symbolPriceTicker[`${symbol}USDT`] &&
COIN_INFO[symbol] && (
<TouchableOpacity
key={`c-${symbol}`}
style={screenStyles.assetSelector.selector}
onPress={() => this.selectAsset(symbol)}
>
<View style={{flexDirection: 'row'}}>
<Image
style={screenStyles.assetSelector.coinAvatar}
source={COIN_INFO[symbol] ? COIN_INFO[symbol].avatar_32 : null}
/>
<Text style={screenStyles.assetSelector.coinName}>
{COIN_INFO[symbol].name}
</Text>
</View>
</TouchableOpacity>
),
)}
</ScrollView>
Its may be the problem with props in the Model which is related to scroll.
as you use ScrollView inside the Model, assuming that , you need a dismissable scrollview
Please checkout a sample related to that : https://gist.github.com/mmitchellgarcia/6ee4d5b5c79162e3cc13b67d743777e6#file-dismissablescrollview-js
For more : https://github.com/react-native-community/react-native-modal/issues/109
Hope it helps

React Native Component not re-rendering despite render function running

I am a little baffled here ..
I have some code like this
renderTopic(topic){
if(topic.isMine){console.log('work?')}
return (
<View key={topic.id.toString()} >
<TouchableOpacity onPress={() => this.addToic.call(this, topic)} >
{topic.isMine && <Topic topic={topic} active={true}/>}
{!topic.isMine && <Topic topic={topic} active={false}/>}
</TouchableOpacity>
</View>
)
}
render() {
console.log("re-rendering", this.props.myTopics)
return (
<View style={s.welcome}>
<ScrollView>
<View style={s.topics}>
{this.props.me.myTopics.map(topic => this.renderTopic(topic) )}
</View>
</ScrollView>
</View>
)
}
The problem is when I update topics using the traditional redux -> 'dispatch action' method, the re-render function fires, the should component update function fires, everything looks like it works but the topic.isMine conditional rendering never updates and switches the components over.
The correct result is displayed if I change page, the app then re-renders correctly.
Probably your reducer does not create new topic object. React sees, that reference to new topic matches old topic and skips calling renderTopic(topic) with same reference value as before.

Resources