Get height of variable children in a functional component in React Native - react-native

I am trying to get the height of children in my component in order to implement an animation.
This is the card component
Expanded with children
I'd like to get the height of the children components so I can animate a height value in order to achieve an animated expansion.
{expanded ? (
<>
<Animated.View style={{height: someAnimatedHeight}}>
<View onLayout={handleLayout}>
{children}
</View>
</Animated.View>
<PromoCodeArea />
</>
) : (
<PromoCodeArea />
)}
handleLayout
const handleLayout = ({ nativeEvent }) => {
setContainerHeight(nativeEvent.layout.height);
};
However, handleLayout doesn't provide the height until the card is expanded. I can get the animation to work with a hardcoded size.
I'd like the animation to be flexible and expand to the height of the children.
When not expanded handleLayout returns a value of 0, therefore I can't set the toValue:
const startAnimation = () => {
Animated.spring(heightAnimation, {
toValue: 125, <-- like this to be based on the height of children
}).start();
};
I haven't been able to find examples of this being achieved using functional components and hooks.

One strategy (which isn't pretty but works for me) is to initially set the children style to opacity 0 (so no-one sees it) and position 'absolute' so it doesn't interfere with your visible layout and don't set the height to 0 so it renders at the required size. Once it's been rendered (consider using useLayoutEffect) get the height from onLayout and save it - then set the style back to how you have it at the moment before anyone gets the chance to interact with it.

Related

React Native: Is there a way to make components scrollable/sticky without using ScrollView?

New to react native. Here is what I am trying to do:
Render a page with three components: top panel, middle row, and content box.
When the user scrolls down, the top panel is scrolled and disappears, middle row is scrolled but is sticky to the top of the screen, and the content box is scrolled all the way down until the end of the content.
Below code serves my intention. However, when I use this code, I get warnings about nesting virtualized views.
return (
<View style={style.profileContainer}>
<ScrollView
stickyHeaderIndices={[1]}
showsVerticalScrollIndicator={false}
>
<TopPanel /> // This is a view component and is scrolled up as the user scrolls down.
<MiddleRow /> // This is a view component that is sticky to the top of the screen
<BottomArray /> //This is a FlatList
</ScrollView>
</View>
)
Below code gets rid of the warning but all the scroll/sticky behaviors of the top/middle components disappear. They just remain fixed as the user scrolls down.
return (
<View style={style.profileContainer}>
<SafeAreaView style={{flex:1}}>
<TopPanel /> // This is a view component and is fixed as the user scrolls down.
<MiddleRow /> // This is a view component and is fixed as the user scrolls down.
<BottomArray /> //This is a FlatList
</SafeAreaView>
</View>
)
Is there a way to make the top panel scrollable and middle row sticky without relying on ScrollView? This is one of the key interfaces of the app and I'd like to keep it alive.
Thanks!
I think if you put the Middle Row inside a Scroll View, that will help. I am new to react-native, so I am pretty sure this won't help you a lot and I think you would have already tried it.
If i understand correctly what you are looking to achieve, you can wrap the 3 boxes inside a view, have the top and middle inside a view and the content inside a scrollview:
<View>
<View><Text>TOP SECTION</Text></View>
<View><Text>Sticky section</Text></View>
<ScrollView><Text>...Content</Text></ScrollView>
</View>
then using animate you can animate the size (for the top section) and the position (of the sticky section) based on the scroll position of the content section.
first you define you scrollY as a ref:
const scrollY= useRef(new Animated.Value(0)).current;
then you convert the views to Animated and "link" the scrollY ref to the scrolling of the scrollview:
<View>
<Animated.View><Text>TOP SECTION</Text></Animated.View>
<Animated.View><Text>Sticky section</Text></Animated.View>
<Animated.ScrollView
scrollEventThrottle={16}
onScroll={Animated.event(
[{ nativeEvent: { contentOffset: { y: scrollY } } }], // attach the scroll to scrollY
{ useNativeDriver: true },
)}>
>
<Text>...Content</Text>
</Animated.ScrollView>
</View>
at this point you can define the function that handle position, size, opacity and so on based on the scrollY position, assuming you know the header height, this function interpolate (map the values) the scroll position of the scrollview from 0 to the height of the header, giving an innverse output range (from 0 to -header heigth):
const headerTranslateY = scrollY.interpolate({
inputRange: [0, HEADER_HEIGHT],
outputRange: [0, -HEADER_HEIGHT],
extrapolate: 'clamp',
});
this function can be attached to the trasform property of the style of the view that you want to animate:
<Animated.View styles={[styles.customstyle, {transform: [{ translateY: headerTranslateY }]}]><Text>TOP SECTION</Text></Animated.View>
basically with interpolate you constatly set values of something, based (in this case) of the scrolling offset of your scrollview (you can read more here on interpolation https://reactnative.dev/docs/animations#interpolation)

'elevation'/'zIndex' prop on React Native View not working on Android

I'm trying to add a hit slop to some icons and I want them to persist even if the icon is hidden under another component.
This is already working fine on iOS with the zIndex prop, but neither the zIndex or elevation prop seem to be having any effect on Android. The icon is still clickable, just without the hit slop surrounding it. This is likely not an issue with the hit slop, as it works fine on a screen alone when not inside of a component.
export const RemoveIndicator = ({ icon, onPress }) => (
<View
style={ {
elevation: 2,
zIndex: 99,
} }
>
<TouchableIcon
color={ colors.Red500 }
name={ icon || 'Circle_Remove_Solid' }
onPress={ onPress }
size={ 16 }
/>
</View>
);
The TouchableIcon component is a component that contains a glyph (given the color, name, and size), wrapped in a TouchableOpacity that takes an onPress function. The hit slop is calculated inside the TouchableIcon based on the size prop.
I expect the icon to be clickable in an area around it, but it is only clickable on the visible glyph itself.

How to round button before rendering?

I want to create a button component that will automatically have rounded corners, no matter its dimension.
As you know, to achieve rounded corners, one way to achieve it is to specify the border radius as half of the height of the button.
The way I implemented is that in the custom component I use the onLayout function like this:
onLayout(event: LayoutChangeEvent) {
const { height } = event.nativeEvent.layout;
this.setState({ borderRadius: height / 2 });
}
The problem is that the button will initially appear on screen as a rectangle and only after a millisecond, it will round the corners causing a flicker.
My guess is that onLayout is called after the component renders.
How would one go about implementing this? Thanks!
Before the borderRadius is calculated, you could return transparent button, this would prevent this flickering effect...
// you pass radius, and height from component state
const MyButton = ({ radius, height }) => {
if (radius === null) return <View style={{ backgroundColor: transparent }}>...</View>
else return <View style={{ borderRadius: radius, backgroundColor: 'red' }}>...</View>;
};
To do this precisely, you would need to know the size that the string would take up once rendered. I wasn't able to find a React Native API for this (and I'm assuming you couldn't either), but I know both Android and iOS have such APIs. So therefore the solution would be to create a native module (https://facebook.github.io/react-native/docs/native-modules-android.html) for in iOS and Android which exposes a method called "measureText" or something. Then in each native class you'd use the corresponding API:
Android API mentioned in this answer should work: https://stackoverflow.com/a/7329169/3930970
iOS API: https://developer.apple.com/documentation/foundation/nsstring/1531844-sizewithattributes
I haven't actually tried this so I'm curious if something like this ends up working. Cheers!
You can use the lifecycle method componentWillMount() to calculate the border radius:
componentWillMount() {
const { height } = Dimensions.get('window');
radius = height / 2;
}
This method is only called one time, which is before the initial
render. Since this method is called before render().
And then, you can style your button using the calculated radius:
<TouchableOpacity
style={[styles.button, { borderRadius: radius }]}
onPress={() => alert('Hello World!') }
>
Here is a working demo.

React native flatlist initial scroll to bottom

I am trying to create a chat in React native using a <Flatlist />
Like WhatsApp and other chat apps, the messages start at the bottom.
After fetching the messages from my API, I call
this.myFlatList.scrollToEnd({animated: false});
But it scrolls somewhere in the middle and sometimes with fewer items to the bottom and sometimes it does nothing.
How can I scroll initially to the bottom?
My chat messages have different heights, so I can't calculate the height.
I had similar issue. If you want to have you chat messages start at the bottom, you could set "inverted" to true and display your messages and time tag in an opposite direction.
Check here for "inverted" property for FlatList. https://facebook.github.io/react-native/docs/flatlist#inverted
If you want to have you chat messages start at the top, which is what I am trying to achieve. I could not find a solution in FlatList, because as you said, the heights are different, I could not use getItemLayout which make "scrollToEnd" behave in a strange way.
I follow the approach that #My Mai mentioned, using ScrollView instead and do scrollToEnd({animated: false}) in a setTimeout function. Besides, I added a state to hide the content until scrollToEnd is done, so user would not be seeing any scrolling.
I solved this issue with inverted property and reverse function
https://facebook.github.io/react-native/docs/flatlist#inverted
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reverse
<FlatList
inverted
data={[...data].reverse()}
renderItem={renderItem}
keyExtractor={(item) => item.id}
/>
You can use this solution in chat component.
I faced the same issue with you and then I moved to use ScrollView.
It is fixed:
componentDidMount() {
setTimeout(() => {
this.scrollView.scrollToEnd();
});
}
<ScrollView ref={(ref) => { this.scrollView = ref; }} style={styles.messages}>
{
messages.map((item, i) => (
<Message
key={i}
direction={item.userType === 'banker' ? 'right' : 'left'}
text={item.message}
name={item.name}
time={item.createdAt}
/>
))
}
</ScrollView>`
Set initialScrollIndex to your data set's length - 1.
I.e.
<Flatlist
data={dataSet}
initialScrollIndex={dataSet.length - 1}
/>
There are two types of 'good' solutions as of 2021.
First one is with timeout, references and useEffect. Here's the full example using Functional Components and Typescript:
// Set the height of every item of the list, to improve perfomance and later use in the getItemLayout
const ITEM_HEIGHT = 100;
// Data that will be displayed in the FlatList
const [data, setData] = React.useState<DataType>();
// The variable that will hold the reference of the FlatList
const flatListRef = React.useRef<FlatList>(null);
// The effect that will always run whenever there's a change to the data
React.useLayoutEffect(() => {
const timeout = setTimeout(() => {
if (flatListRef.current && data && data.length > 0) {
flatListRef.current.scrollToEnd({ animated: true });
}
}, 1000);
return () => {
clearTimeout(timeout);
};
}, [data]);
// Your FlatList component that will receive ref, data and other properties as needed, you also have to use getItemLayout
<FlatList
data={data}
ref={flatListRef}
getItemLayout={(data, index) => {
return { length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index };
}}
{ ...otherProperties }
/>
With the example above you can have a fluid and animated scroll to bottom. Recommended for when you receive a new message and has to scroll to the bottom, for example.
Apart from this, the second and easier way is by implementing the initialScrollIndex property that will instantly loads the list at the bottom, like that chat apps you mentioned. It will work fine when opening the chat screen for the first time.
Like this:
// No need to use useEffect, timeout and references...
// Just use getItemLayout and initialScrollIndex.
// Set the height of every item of the list, to improve perfomance and later use in the getItemLayout
const ITEM_HEIGHT = 100;
<FlatList
data={data}
getItemLayout={(data, index) => {
return { length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index };
}}
{ ...otherProperties }
/>
I found a solution that worked for me 100%
Added the ref flatListRef to my flatlist:
<Flatlist
reference={(ref) => this.flatListRef = ref}
data={data}
keyExtractor={keyExtractor}
renderItem={renderItem}
/>
Then whenever you want to automatically scroll to bottom of the list use:
this.flatListRef._listRef._scrollRef.scrollToEnd({ animating: true });
yes you should access the element _listRef then _scrollRef then call the scrollToEnd 🙄
react-native 0.64.1
react 17.0.2
I've struggled on this as well and found the best possible solution for me that renders without a glitch is:
Use inverted={-1} props
Reverse the order of messages objects inside my array with data={MyArrayofMessages.reverse()} in my case data={this.state.messages.reverse()} using reverse() javascript function.
Stupidly easy and renders instantaneously !
Use inverted={1} and reverse your data by using the JS reverse function. It worked for me
<FlatList contentContainerStyle={{ flex: 1, justifyContent: 'flex-end' }} />
I am guessing that RN cannot guess your layout so it cannot know how much it needs to "move". According to the scroll methods in the docs you might need to implement a getItemLayout function, so RN can tell how much it needs to scroll.
https://facebook.github.io/react-native/docs/flatlist.html#scrolltoend
Guys if you want FlatList scroll to bottom at initial render. Just added inverted={-1} to your FlatList. I have struggle with scroll to bottom for couple of hours but it ends up with inverted={-1}. Don't need to think to much about measure the height of FlatList items dynamically using getItemLayout and initialScrollIndex or whats so ever.
I found a solution that worked for me 100%
let scrollRef = React.useRef(null)
and
<FlatList
ref={(it) => (scrollRef.current = it)}
onContentSizeChange={() =>
scrollRef.current?.scrollToEnd({animated: false})
}
data={data}/>
If you want to display the message inverted, set "inverted" to true in the flat list.
<Flatlist
data={messageData}
inverted={true}
horizontal={false}
/>
If you just want to scroll to the last message, you can use initialScrollIndex
<Flatlist
data={messageData}
initialScrollIndex={messageArray.length - 1}
horizontal={false}
/>
I spent couple of hours struggling with showing the first message on top without being able to calculate the item's height as it contains links and messages. But finally i've been able to...
What i've done is that i wrapped the FlatList in a View, set FlatList as inverted, made it to take all available space and then justified content. So now, conversations with few messages starts at top but when there are multiple messages, they will end on bottom. Something like this:
<View style={ConversationStyle.container}>
<FlatList
data={conversations}
initialNumToRender={10}
renderItem={({ item }) => (
<SmsConversationItem
item={item}
onDelete={onDelete}
/>
)}
keyExtractor={(item) => item.id}
getItemCount={getItemCount}
getItem={getItem}
contentContainerStyle={ConversationStyle.virtualizedListContainer}
inverted // This will make items in reversed order but will make all of them start from bottom
/>
</View>
And my style looks like this:
const ConversationStyle = StyleSheet.create({
container: {
flex: 1
},
virtualizedListContainer: {
flexGrow: 1,
justifyContent: 'flex-end'
}
};

FlatList calls `onEndReached` when it's rendered

Here is render() function for my simple category list page.
Recently I added pagination for my FlatList View so when the user scrolls to the bottom, onEndReached is called in a certain point(onEndReachedThreshold value length from the bottom), and it will fetch the next categories and concatenate the categories props.
But my problem is onEndReached is called when render() is called In other words, FlatList's onEndReached is triggered before it reach the bottom.
Am I putting wrong value for onEndReachedThreshold? Do you see any problem?
return (
<View style={{ flex:1 }}>
<FlatList
data={this.props.categories}
renderItem={this._renderItem}
keyExtractor={this._keyExtractor}
numColumns={2}
style={{flex: 1, flexDirection: 'row'}}
contentContainerStyle={{justifyContent: 'center'}}
refreshControl={
<RefreshControl
refreshing = {this.state.refreshing}
onRefresh = {()=>this._onRefresh()}
/>
}
// curent value for debug is 0.5
onEndReachedThreshold={0.5} // Tried 0, 0.01, 0.1, 0.7, 50, 100, 700
onEndReached = {({distanceFromEnd})=>{ // problem
console.log(distanceFromEnd) // 607, 878
console.log('reached'); // once, and if I scroll about 14% of the screen,
//it prints reached AGAIN.
this._onEndReachedThreshold()
}}
/>
</View>
)
UPDATE I fetch this.props.categories data here
componentWillMount() {
if(this.props.token) {
this.props.loadCategoryAll(this.props.token);
}
}
Try to implement onMomentumScrollBegin on FlatList :
constructor(props) {
super(props);
this.onEndReachedCalledDuringMomentum = true;
}
...
<FlatList
...
onEndReached={this.onEndReached.bind(this)}
onEndReachedThreshold={0.5}
onMomentumScrollBegin={() => { this.onEndReachedCalledDuringMomentum = false; }}
/>
and modify your onEndReached
onEndReached = ({ distanceFromEnd }) => {
if(!this.onEndReachedCalledDuringMomentum){
this.fetchData();
this.onEndReachedCalledDuringMomentum = true;
}
}
I've got it working with
<Flatlist
...
onEndReached={({ distanceFromEnd }) => {
if (distanceFromEnd < 0) return;
...
}
...
/>
First check if the FlatList is inside a ScrollView or Content of native-base. Then take it outside of it
Actually you don't need to use Content or ScrollView, as FlatList has both ListFooterComponent and ListHeaderComponent.
Though it is not recommended, if you really need to use Flatlist inside ScrollView, then take a look at this answer: https://stackoverflow.com/a/57603742/6170191
After hours of trying different approaches I got it to work by wrapping the Flatlist with a View of fixed height and flex:1.
With this settings, I was able to get onEndReached called once and only after I scroll near the bottom. Here's my code sample:
render() {
const {height} = Dimensions.get('window');
return (
<View style={{flex:1, height:height}}>
<FlatList
data={this.props.trips_uniques}
refreshing={this.props.tripsLoading}
onRefresh={()=> this.props.getTripsWatcher()}
onEndReached={()=>this.props.getMoreTripsWatcher()}
onEndReachedThreshold={0.5}
renderItem={({item}) => (
<View style={Style.card}>
...
...
</View>
)}
keyExtractor={item => item.trip_id}
/>
</View>
)
}
My onEndReached() function just calls the API and updates my data. It doesn't do any calculations with regards to distance to bottom or threshold
Most of the times, this error is caused because of an incorrect use of onEndReachedThreashold, which also depends of the number of items you are rendering (more items, more scroll size).
Try to follow this logic:
If 10 items cover your screen, and you are rendering 20 items on each scroll, then set onEndReachedThreashold to 0.8.
If 2 or 3 items cover your screen, and you are rendering 10 items on each scroll, then set onEndReachedThreashold to 0.5.
Also, use initialNumToRender = numItems. For some reason, using this FlatList prop helps to reduce the chance of multiple onEndReached calls.
Just play with onEndReachedThreashold value.
Other times, this error is produced because of nesting scroll views. Do not put your FlatList inside of a ScrollView. Instead, take use of the FlatList header and footer props.
For both solutions, I suggest to set the FlatList style and contentContainerStyle to { flexGrow: 1 }.
Remove every Scrollable View inside your FlatList
If you want to show 3 or 4 records and want to load the next data just when you reach the end. Set onEndReachedThreshold to 0 or 0.1.
Maybe You can bypass this FlatList bug by incrementing your page before doing async call, and then you will fetch data on every onEndReached fiers and not get errors about duplicate keys
(as of NOV19)
Keep flatlist as the only component inside of a single view
Set style of that single view from dimensions like
{{flex: 1, height: Dimensions.get('window').height}}
If FlatList is on another FlatList or ScrollView the onEndReached call immediately when rendered component to resolve that problem doesn't wrap FlatList with another.
A bit late but I just ran into this issue and I fixed it by passing to my <FlatList/> the initialNumToRender prop. This prop is 10 by default so if you don't set it and your screen shows more than 10 items on the initial render, it is going to trigger onEndReached since it has passed the 10th element.
initialNumToRender should probably be the same as the amount of elements you fetch per page.
I have a <FlatList> (from react-native) inside an <Overlay> (from react-native-elements.) I have the problem of onEndReached being executed as soon as the component is rendered for the 1st time and before the user does anything.
The problem was resolved by using <Modal> (from react-native), instead of <Overlay>.
If you are using hooks, here you can find the hook version of #Ilario answer:
const onEndReachedCalledDuringMomentum = useRef(true)
onEndReachedHandler = ({ distanceFromEnd }) => {
if(!onEndReachedCalledDuringMomentum.current){
fetchData()
onEndReachedCalledDuringMomentum.current = true
}
}
<FlatList
...
onEndReached={onEndReachedHandler}
onEndReachedThreshold={0.7}
onMomentumScrollBegin={() => { onEndReachedCalledDuringMomentum.current = false }}
/>
This simple solution worked for me. Note the "refreshing" state is controlled by an async API call in a useEffect hook to retrieve data for the FlatList.
const onEndReachedHandler = () => {
if (!refreshing) ...
}
<FlatList
...
data={mydata}
onEndReached={onEndReachedHandler}
onEndReachedThreshold={0.7}
refreshing={refreshing}
/>
I struggled around the whole day but the issue that I was getting is, I am using FlatList inside ScrollView. So, Remove Scrollview & then use Flatlist independently. This will solve my problem.
From my experience, you can simply utilize onEndReachedThreshold props in your FlatList or SectionList and pass a very very small number like 0.001 to it.
onEndReachedThreshold={0.001}
According to docs for FlatList, onEndReachedThreshold is units of length from the bottom in list items.
How far from the end (in units of visible length of the list) the
bottom edge of the list must be from the end of the content to trigger
the onEndReached callback. For example, a value of 0.5 will trigger
onEndReached when the end of the content is within half the visible
length of the list.
Thus, a very small value like 0.001 helps you to make sure that onEndReached is only gonna be called when the end of the content is within the very end of the visible length of the list.
Hope this helps :) Sorry for bad English.
The solution is simpler than anyone would think.
Just add an !isLoading condition for fetch calling. It works for me:
onEndReached={() => {
if (!isLoading) {
fetchProducts();
}
}}
And the full code with ScrollView and FlatList:
<ScrollView
horizontal={true}
showsVerticalScrollIndicator={false}
showsHorizontalScrollIndicator={false}
style={styles.mainContainer}>
<FlatList
ListHeaderComponent={TableHeader}
data={displayedProducts}
renderItem={(item: ListRenderItemInfo<IProduct>) => TableRow(item, showDeleteModal)}
keyExtractor={keyExtractor}
ListFooterComponent={<Loading loadingText={'Loading products...'} />}
onEndReached={() => {
if (!isLoading) {
fetchProducts();
}
}}
onEndReachedThreshold={0.8}
stickyHeaderIndices={[0]}
/>
</ScrollView>
I have solved it with using debounce from lodash. Firstly, I import debounce from 'lodash.debounce'. Then I use debounce for load more function with 500 ms interval
<Flatlist
onEndReached = {debounce(this._onLoadMore, 500)}
/>