Flatlist scroll references with getItemLayoutProp - reactjs

I've got a little problem with flatlist and scroll methods.
I have flatlist with comments and if new one is added I want to scroll list to the bottom to see added comment.
Using scrollToIndex doesn't work properly, native keep showing errors due to lack of getItemLayout, and here is another problem with setting this function - every item can have different size.
scrollToEnd has some issues, sometimes it scrolls to almost bottom of the page, sometimes to headerComponent elements which are set in flatlist props.
Do you have any ideas how to make it scroll to the bottom?

To use scrollToIndex you need to use getItemLayout. There is no point in using it if you have no intention to use getItemLayout. Here is an example taken from the react-native docs:
class ScrollToExample extends Component {
getItemLayout = (data, index) => (
{ length: 50, offset: 50 * index, index }
)
scrollToIndex = () => {
let randomIndex = Math.floor(Math.random(Date.now()) *
this.props.data.length);
this.flatListRef.scrollToIndex({animated: true, index:
randomIndex});
}
scrollToItem = () => {
let randomIndex = Math.floor(Math.random(Date.now()) *
this.props.data.length);
this.flatListRef.scrollToIndex({animated: true, index: "" +
randomIndex});
}
render() {
return (
<FlatList
style={{ flex: 1 }}
ref={(ref) => { this.flatListRef = ref; }}
keyExtractor={item => item}
getItemLayout={this.getItemLayout}
initialScrollIndex={50}
initialNumToRender={2}
renderItem={({ item, index}) => (
<View style={{...style, backgroundColor: this.getColor(index)}}>
<Text>{item}</Text>
</View>
)}
{...this.props}
/>
);
}
}
https://gist.github.com/joshyhargreaves/b8eb67d24ce58a6d8bffb469f7eeaf39
Hope this helps!

Related

How to make two ScrollViews synchronously scroll simultaneously on React Native?

I have two ScrollViews. When I scroll one the other should also scroll.
I tried using onScroll event, but it has a delay, the second ScrollView scrolls after a while. I really need it to scroll exactly at the same time.
Is there any other way to do it?
let scroll_ref_1 = null;
let scroll_ref_2 = null;
const Page = () => {
return (
<SafeAreaView>
<ScrollView
ref={ref => scroll_ref_1 = ref}
horizontal
onScroll={e => {
if(scroll_ref_2 !== null) {
scroll_ref_2.scrollTo({
x: e.nativeEvent.contentOffset.x,
animated: false,
});
}
}}
>
...
</ScrollView>
<ScrollView
horizontal
ref={ref => scroll_ref_2 = ref}
>
...
</ScrollView>
</SafeAreaView>
);
};
You can get a similar effect if you use flatlist with 2 columns.

How can I rerender only one item in a flatlist?

I have products with a star icon to add this product in wishlist. I map 10 list of products and each map has 3 products like:
(I Map it in Pagerview to swipe to the next products)
Products Component
const ListProducts = [
{
id: 1,
products: [{
product_id: 1,
photos: [...]
}]
},
{
id: 2,
products: [{
product_id: 1,
photos: [...]
}]
}
{
id: 3,
products: [{
product_id: 1,
photos: [...]
}]
},
{
id: 4,
products: [{
product_id: 1,
photos: [...]
}]
}
...
];
function isEq(prev, next) {
if(prev.is_wishlist === next.is_wishlist) {
return true;
}
}
const Item = memo(({ id, photos, is_wishlist, onPress, onPressWishlist }) => {
const findProductIdInWishList = is_wishlist.find((el => el.product_id === id));
return (
<Button style={s.imgBtn} onPress={() => onPress(id)}>
<Button onPress={() => onPressWishlist(id)} style={s.starBtn}>
<AntDesign name={findProductIdInWishList ? 'star' : 'staro'} size={20} color={globalStyles.globalColor} />
</Button>
<ImageSlider photos={photos} />
</Button>
)
// #ts-ignore
}, isEq);
const wishlist = useSelector((state) => state.WishList.wishlist);
const dispatch = useDispatch();
const renderItem: ListRenderItem<IProduct> = ({ item }) => (
<Item
id={item.id}
photos={item.photos}
is_wishlist={wishlist}
onPressWishlist={handlePressWishList}
/>
)
const handlePressWishList = (product_id: string) => {
dispatch(addAndRemoveProductToWishList({product_id}));
};
List of Products component
Products Map:
<PagerView onPageSelected={(e) => handleSetAllIndexes(e.nativeEvent.position)} style={s.container} initialPage={index}>
{
allProducts.map((el, i) => (
<View style={s.viewsContainer} key={i}>
{ allIndex.includes(i) ? (
<View style={s.viewsInnerContainer}>
{ /* products */ }
<Products products={el.products as IProduct[]} total_price={el.total_price} product_name={el.packet_name} />
</View>
) : (
<View style={s.loadingContainer}>
<Loader size={'large'} color='#fff' />
</View>
)
}
</View>)
)
}
</PagerView>
if I click on star icon its dispatch and it goes fast but if I swipe to other products maybe to the last, then I press the star icon to dispatch then its a delay/lag you can see it
I dont add the full code because there are some snippets that has nothing to do with problem.
PS:
Video
I think there are a few issues in your code:
1. Wrong dependency list for useMemo.
In your Item component, you should pass the list of dependency, rather than a compare function:
const Item = memo(({ id, photos, is_wishlist, onPress, onPressWishlist }) => {
...
// #ts-ignore
}, isEq); // <- this is wrong
// Instead, you should do this:
}, [is_wishlist]); // <- this is correct, if you only want to update Item component when `is_wishlist` is changed
2. Never use index as key if item can be reordered
In your products maps component, you are doing:
allProducts.map((el, i) => (
<View style={s.viewsContainer} key={i}>
You should pass id instead, so React will not re-render all items when you insert a new item at the beginning or in the middle:
<View style={s.viewsContainer} key={el.id}>
3. Passing wishlist to all Items, however, wishlist will be updated whenever user click star button on any item.
This causes all Item to re-generate memoized component, because wishlist is changed.
What you want to do here is only passing essential data to Item, which is inWishList (or findProductIdInWishList in your code), which only get changed for affected item:
const renderItem: ListRenderItem<IProduct> = ({ item }) => {
const inWishList= wishlist.find((el => el.product_id === id));
return (
<Item
id={item.id}
photos={item.photos}
inWishList={inWishList}
onPressWishlist={handlePressWishList}
/>
)
}
I am going to edit my answer instead of comment. Before my code, let me explain first. In your current code, whenever 'allProducts' changes, everything will re-render. Whenever 'allIndex' changes, everything will re-render too. The longer the list, the more lag it will be.
Can you try 'useCallback' in this case?
const renderItem = React.useCallback((el, i) => (
<View style={s.viewsContainer} key={i}>
{allIndex.includes(i) ? (
<View style={s.viewsInnerContainer}>
<Products />
</View>
) : (
<Loading />
)}
</View>
),[allIndex])
{allProducts.map(renderItem)}
Now, renderItem will re-render when 'allIndex' changes. Instead of 'PagerView', I still recommend 'FlatList' with 'horizontal={true}' and 'some styles'. If still wanna use 'PagerView', how about 'Lazy' components? 'Lazy components' does not render the components before they came into user view. So, before they are seen, they do not take part in re-redner.
The issue is connected to a way how you edit your data to hold the new value. It looks like the act of adding an item to a wishlist causes all the previous items to re-render.
Therefore the first one works without an issue, but the last one takes a while as it needs to re-render all the other items before.
I would start by changing the key from index to an actual ID of a product block since that could prevent the other "pages" from re-rendering.
If that fails you will probably need to take this block of code into a workshop and check for useless re-renders.

How to prevent the entire list of items from re-rendering when only one item has changed? (React Native with hooks)

I have been working with lists in React Native (hooks edition). I have a function that returns a list of items like this.
function toDisplay (what) {
let dataList = [];
if (what === 'List 1') dataList = MobXStore.listOne
else if (what === 'List 2') dataList = MobXStore.listTwo
return (
<ScrollView>
{
dataList ? dataList.map((item,index) =>
<ListItem
key={index + item.id}
containerStyle={{
backgroundColor: index % 2 === 0 ? 'black' : '#0a0a0a',
}}
leftAvatar={{source: {uri: item.image}, size: 75}}
title={item.title}
}
</ScrollView>
)
}
When there's a change in only one time in the list. React seems to re-render the entire list. This seems to take place not each time though. Is there a way to affect only that item each time?
It's bettter to replace your Scrollview with Flatlist:
<FlatList
data={dataList}
renderItem={({item}) => <Text>{item.data}</Text>}
keyExtractor={item => `listkey${item.id}`}
/>
And using keyExtractor prop, you avoid re-rendering the whole list every time an item is added to the list

Crash when doing scrollToLocation on SectionList

We have an edge case in our app. After the UI is rendered and the user tries to scroll to a section it throws scrolltoindex should be used in conjunction with getitemlayout or on scrolltoindex failed. Now this happens only when he does this immediately after UI render.
_scrollToSection = index => {
setTimeout(() => {
this.list.scrollToLocation({
animated: true,
itemIndex: -1,
sectionIndex: index,
viewPosition: 0
});
}, 150);
};
Section list render:
<SectionList
sections={this.props.sections}
extraData={this.props.subscriber}
ref={ref => {
if (ref) {
this.list = ref;
}
}}
automaticallyAdjustContentInsets={true}
contentInsetAdjustmentBehavior={'automatic'}
windowSize={100}
ListHeaderComponent={this.props.header || null}
ItemSeparatorComponent={() => (
<Separator
style={[mediumStyle.separatorEnd, { backgroundColor: IOS_GREY_02_03 }]}
/>
)}
renderSectionFooter={() => <View style={{ height: 17 }} />}
keyExtractor={(item, index) => index}
removeClippedSubviews={false}
stickySectionHeadersEnabled={true}
renderSectionHeader={({ section }) => (
<SectionTitle title={section.title} theme={this.props.theme} />
)}
renderItem={this._renderItem}
onEndReachedThreshold={0}
onEndReached={() => HapticFeedback.trigger()}
scrollEventThrottle={16}
/>
I tried to google the cause but was unsuccessful finding only outdated and closed issues without a solution. Did this happen to anybody else? How did you fixed it?
UPDATE:
We have come up on a solution of constant item sizes which also takes in count the accessibility scale factor. So we had an item and header size we could use in getItemLayout. All worked as should, but the SectionList is glitchy. When we scrolled to lower sections, the list was jumpy by itself without any interaction.
So far the best solution we had was to build the section list ourselves in native code and use that instead of the RN list.
You're getting this error because scrollToIndex has failed and you have not implemented getItemLayout or onScrollToIndexFailed
getItemLayout in a section list is not exactly fun to setup however this medium post goes over how to do it https://medium.com/#jsoendermann/sectionlist-and-getitemlayout-2293b0b916fb
They suggest react-native-section-list-get-item-layout to calculate the sizes of the layout https://github.com/jsoendermann/rn-section-list-get-item-layout
onScrollToIndexFailed is easier to setup you can add the prop onScrollToIndexFailed={(info) => { /* handle error here /*/ }} You can catch the error and then decide on how you will handle it here.
I would also add a check to make sure that your reference to this.list exists before calling the scrollToLocation function. Something like this.
_scrollToSection = index => {
setTimeout(() => {
if (this.list) {
this.list.scrollToLocation({
animated: true,
itemIndex: -1,
sectionIndex: index,
viewPosition: 0
});
}
}, 150);
};

React native infinity view pager

I need to create infinity view pager to display calendar days, and add an ability to user for swapping left/right and changing date.
As I see in the documentation, the view pager will work only with preset number of views, and also research some opensource packages - cant find anything about that.
So my question - how can I implement infinity swiping for calendar (or is it possible at all)?
I have an infinite viewpager made with VirtualizedList. It works on iOS an Android.
import React, { Component } from 'react'
import { View, Text, Dimensions, VirtualizedList } from 'react-native'
const { width, height } = Dimensions.get('window')
const startAtIndex = 5000
const stupidList = new Array(startAtIndex * 2)
class InfiniteViewPager extends Component {
//only use if you want current page
_onScrollEnd(e) {
// const contentOffset = e.nativeEvent.contentOffset
// const viewSize = e.nativeEvent.layoutMeasurement
// // Divide the horizontal offset by the width of the view to see which page is visible
// const pageNum = Math.floor(contentOffset.x / viewSize.width)
}
_renderPage(info) {
const { index } = info
return (
<View style={{ width, height }}>
<Text>
{' '}{`index:${index}`}{' '}
</Text>
</View>
)
}
render() {
return (
<VirtualizedList
horizontal
pagingEnabled
initialNumToRender={3}
getItemCount={data => data.length}
data={stupidList}
initialScrollIndex={startAtIndex}
keyExtractor={(item, index) => index}
getItemLayout={(data, index) => ({
length: width,
offset: width * index,
index,
})}
maxToRenderPerBatch={1}
windowSize={1}
getItem={(data, index) => ({ index })}
renderItem={this._renderPage}
onMomentumScrollEnd={this._onScrollEnd}
/>
)
}
}

Resources