I'm using React Native (0.68) and Firebase RTDB (with the SDK, version 9), in Expo.
I have a screen that needs to pull a bunch of data from the RTDB and display it in a Flatlist.
(I initially did this without Flatlist, but initial rendering was a bit on the slow side.)
With Flatlist, initial rendering is super fast, huzzah!
However, I have an infinite loop re-render that I'm having trouble finding and fixing. Here's my code for the screen, which exists within a stack navigator:
export function GroupingsScreen () {
... set up a whole bunch of useState, database references (incl groupsRef) etc ...
onValue(groupsRef, (snapshot) => {
console.log('groups onValue triggered')
let data = snapshot.val();
if (loaded == false) {
console.log('--start processing')
setLoaded(true);
let newObject = []
for (let [thisgrouping, contents] of Object.entries(data)) {
let onegroupingObject = { title: thisgrouping, data: [] }
for (let [name, innerdata] of Object.entries(contents.ingredients)) {
onegroupingObject.data.push({ name: name, sku: innerdata.sku, size: innerdata.size,
quantity: innerdata.quantity,
parent: thisgrouping
})
}
newObject.push(onegroupingObject)
}
console.log('--done processing')
setGroupsArray(newObject)
}
});
.... more stuff excerpted ....
return (
<View style={styles.tile}>
<SectionList
sections={groupsArray}
getItemLayout={getItemLayout}
renderItem={ oneRender }
renderSectionHeader={oneSection}
initialNumToRender={20}
removeClippedSubviews={true}
/>
</View>
)};
I'm using loaded/setLoaded to reduce re-renders, but without that code, RN immediately dumps me out for excessive re-renders. Even with it, I get lots of extra renders.
So...
Can someone point me at what's triggering the rerender? The database is /not/ changing.
Is there a better way to get RTDB info into a Flatlist than the code I've written?
I have some code that actually does change the database. That's triggering a full rerender of the whole Flatlist, which is visibly, painfully slow (probably because parts are actually rendering 10x instead of once). Help?
For completeness, here's the OneItem code, so you can see just how complex my Flatlist items are:
const OneItem = (data) => {
// console.log('got data',data)
return (
<View style={[styles.rowView, { backgroundColor: data.sku?'white': '#cccccc'}]} key={data.name}>
<TouchableOpacity style={styles.nameView} onPress={() => {
navigation.navigate('AddEditItemScreen', {purpose: 'Grouping', itemname: data.name, parent: data.parent, mode: 'fix'})
}}>
<View style={styles.nameView}>
<Text style={styles.itemtext}>{data.name}</Text>
{data.sku? null: <Text>"Tap to add SKU."</Text>}
{data.size?<Text>{data.size} </Text>: <Text>no size</Text>}
</View>
</TouchableOpacity>
<View style={styles.buttonView}>
<Button style={styles.smallButton}
onPress={() => { changeQuant(data.quantity ? data.quantity - 1 : -1, data.parent + '/ingredients/' + data.name) }}
>
{data.quantity > 0 ? <Text style={[styles.buttonText, { fontSize: 20 }]}>-</Text>
:<Image source={Images.trash} style={styles.trashButton} />}</Button>
<Text style={styles.quantitytext}>{data.quantity}</Text>
<Button style={styles.smallButton}
onPress={() => {
changeQuant(data.quantity? data.quantity +1 : 1, data.parent+'/ingredients/'+data.name)}}>
<Text style={[styles.buttonText, {fontSize: 20}]}>+</Text></Button>
</View>
</View>
)
};```
I worked out how to stop the rerender (question #1). So, within my Screen functional component, I needed to make another function, and attach the state hook and useEffect to that. I'm not totally sure I understand why, but it gets rid of extra renders. And it's enough to get #3 to tolerable, although perhaps not perfect.
Here's the new code:
export function GroupingsScreen () {
... lots of stuff omitted ...
function JustTheList() {
const [groupsArray, setGroupsArray] = useState([])
useEffect(() => {
const subscriber = onValue(groupsRef, (snapshot) => {
console.log('groups onValue triggered')
let data = snapshot.val();
let newObject = []
for (let [thisgrouping, contents] of Object.entries(data)) {
let onegroupingObject = { title: thisgrouping, data: [] }
for (let [name, innerdata] of Object.entries(contents.ingredients)) {
onegroupingObject.data.push({ name: name, sku: innerdata.sku, size: innerdata.size,
quantity: innerdata.quantity,
parent: thisgrouping
})
}
newObject.push(onegroupingObject)
}
setGroupsArray(newObject)
})
return () => subscriber();
}, [])
return(
<View style={styles.tile}>
<SectionList
sections={groupsArray}
getItemLayout={getItemLayout}
renderItem={ oneRender }
renderSectionHeader={oneSection}
initialNumToRender={20}
removeClippedSubviews={true}
/>
</View>
)
}
And then what was my return within the main functional screen component became:
return (
<JustTheList />
)
I'm still very interested in ideas for improving this code - am I missing a better way to work with RTDB and Flatlist?
Related
Apologies, couldn't figure out a straightforward way to title this, so thanks in advance.
I am tasked with creating a page of links supplied by an API I made that connects to a CMS. The screen gathers the below data to be viewed on the screen:
Title,
Content
These items, along with a URL live within a "Resource". The URL is not visible to the user, but it is grouped with its own Title and Content.
Currently things show correctly on the screen, but when trying to connect the URL to the Resource, I'm unable to have the page navigate correctly. When I console.log(thing-I'm-returning), it sends me back all URLs for all Resources, and if the private browser opens to a web page, it might open to any in the list. This happens when I press any of the Resources.
Code below (first time posting, I'm fully desperate. Let me know if this looks like trash and I'll correct however is ideal).
const { resourceData } = useResourceContent(binding);
const resourceList = resourceData?.Resources?.map((r, i) => ({
id: i.toString(),
title: r.Title,
url: r.Url,
content: r.Content,
}));
const resourceDetails = resourceData?.Resources;
const { openUrl } = useWebBrowser();
const { resourceData } = useResourceContent(binding);
const resourceDetails = resourceData?.Resources;
const urlList = [];
const handleOpenSite = () => {
resourceDetails?.map((r, i) =>
{if (resourceDetails !== undefined && resourceDetails) {
urlList.push(r.Url);
}
console.log(urlList[i]); //let's say there are 2 resources, each with their own website. This will return both websites no matter what resource I select
//the below is required, as a private browser is required
return openUrl(urlList[i]);
});
};
API looks something like:
[{"Content": "Test. ", "Url": "https://instagram.com", "Title": "blah blah blah"},
{{"Content": "Test2.", "Url": "https://google.com.com", "Title": "blah blah blah"},]
Here's the View, though I'm unsure if it's necessary here.
<View>
<ResourceNavigationList
onPress={handleOpenSite}
small
listItems={resourceList}
backgroundColor="transparent"
/>
</View>
And here's the ResourceNavigationList component, which is likely the issue since it's a little bit nonsense.:
const ResourceNavigationList = ({
listItems,
backgroundColor,
small,
reverse,
onPress,
}) => {
const { ct } = useCountryTranslation();
const colorScheme = useColorScheme();
const bgColor = backgroundColor || Colors[colorScheme].altBackground;
const { openUrl } = useWebBrowser();
const renderItem = ({ item, rUrl }) => {
const handleOnPress = () => {
if (item) {
openUrl(rUrl).toString();
console.log("WHY AREN'T YOU OPENING?");
}
};
return (
<ResourceNavigationListItem
key={key}
reverse={reverse}
small={small}
item={item}
// url={rUrl}
onPress={onPress}
/>
);
};
return (
<FlatList
renderItem={renderItem}
data={listItems}
keyExtractor={(item) => item.id}
style={{
paddingVertical: 20,
backgroundColor: bgColor,
}}
/>
);
};
ResourceNavigationList.propTypes = {
listItems: PropTypes.array.isRequired,
backgroundColor: PropTypes.string,
small: PropTypes.bool,
reverse: PropTypes.bool,
onPress: PropTypes.func,
};
export default ResourceNavigationList;
Finally, here's the ResourceNavigationListItem
const ResourceNavigationListItem = ({ item, onPress, style, small }) => {
const styles = StyleSheet.create({
//styling is here, but leaving it off because it isn't relevant and took up a lot of space
});
return (
<TouchableOpacity onPress={onPress} style={[styles.item, style]}>
<View style={styles.title}>
<Icon style={styles.linkArrow} size={16} icon={faExternalLink} />
</View>
<View style={styles.title}>
<Text style={styles.titleText}>
{item.title ? decode(item.title) : item.title}
</Text>
</View>
<View style={styles.title}>
<Text style={styles.titleText}>
{item.content ? decode(item.content) : item.content}
</Text>
</View>
</TouchableOpacity>
);
};
ResourceNavigationListItem.propTypes = {
item: PropTypes.shape({
icon: PropTypes.object,
title: PropTypes.string,
content: PropTypes.string,
}).isRequired,
onPress: PropTypes.func,
style: PropTypes.object,
small: PropTypes.bool,
};
export default ResourceNavigationListItem;
Thanks so very much.
I've tried mapping and for-looping. I've tried applying the mapping directly to the component. These have gleaned me the most success. Most everything else I've tried didn't return anything at all, or returned everything many times.
I've been struggling for a few days and have found lots of solutions similar to my problem within stackoverflow, but nothing fully relevant/recent (I'm fairly newb with regard to backend tingz). If y'all happen upon something I missed, please be kind, and if you'd be down to help me, I'd be so very grateful.
I took a look and I think this has to do with confusion regarding the props hierarchy within your example.
Right now you are passing onPress down from the parent of ResourceNavigationList:
Parent > onPress
ResourceNavigationList
ResourceNavigationListItem
While there is nothing inherently incorrect about the parent owning the onPress function, you have everything you need to perform the desired onPress functionality within the ResourceNavigationList component. It looks like you were almost there with your handleOnPress function.
Based on the data contract you provided, it looks like you should be able to do this:
const ResourceNavigationList = ({
listItems,
backgroundColor,
small,
reverse,
onPress,
}) => {
const {
ct
} = useCountryTranslation();
const colorScheme = useColorScheme();
const bgColor = backgroundColor || Colors[colorScheme].altBackground;
const {
openUrl
} = useWebBrowser();
const renderItem = ({
item,
rUrl
}) => {
const handleOnPress = () => {
if (item && item ? .Url) {
openUrl(item ? .Url);
// If you do need to perform additional logic controlled
// by the parent component, you can add a quick line to
// execute the onPress function if it has been provided.
if (onPress) onPress(item);
}
};
return ( <
ResourceNavigationListItem key = {
key
}
reverse = {
reverse
}
small = {
small
}
item = {
item
}
onPress = {
onPress
}
/>
);
};
return ( <
FlatList renderItem = {
renderItem
}
data = {
listItems
}
keyExtractor = {
(item) => item.id
}
style = {
{
paddingVertical: 20,
backgroundColor: bgColor,
}
}
/>
);
};
ResourceNavigationList.propTypes = {
listItems: PropTypes.array.isRequired,
backgroundColor: PropTypes.string,
small: PropTypes.bool,
reverse: PropTypes.bool,
onPress: PropTypes.func,
};
export default ResourceNavigationList;
I'm trying to reduce the code and/or optimize the use of React state hooks in a form with the rn-material-ui-textfield module. Normally, for a single text field, you could do something like this
import { OutlinedTextField } from 'rn-material-ui-textfield'
// ...and all the other imports
const example = () => {
let [text, onChangeText] = React.useState('');
let [error, set_error] = React.useState('');
const verify = () => {
if(!text) set_error('Enter a text');
else console.log('Finished');
}
return(
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<OutlinedTextField
onChangeText={onChangeText}
value={text}
onSubmitEditing={verify}
error={error}
/>
</View>
);
}
and it would surely work no problem. But as you keep on adding more and more fields, setting a separate error and text hooks for each of them seem tedious and generates a lot of code. So, in order to prevent this, I tried to write this in a different way
// ...all imports from previous snippet
const example = () => {
let [inputs, set_input_arr] = React.useState(['', '', '', '', '', '']);
let [error, set_error_arr] = React.useState(['', '', '', '', '', '']);
const error_names = ['your name', 'an email ID', 'a password', 'your phone number', "your favourite color", 'your nickname'];
const set_input = (index, text) => {
let temp = inputs;
temp[index] = text;
set_input_arr(temp);
};
const set_error = (index, err) => {
let temp = error;
temp[index] = err;
set_error_arr(temp);
// this logs the array correctly after running the loop each time
console.log(`This was set as error: ${error}`);
};
const verify = () => {
for (let i = 0; i < inputs.length; i++) {
if (!inputs[i]) set_error(i, `Please enter ${error_names[i]}`);
}
};
return(
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<OutlinedTextField
onChangeText={text => set_input(0, text)}
value={inputs[0]}
error={error[0]}
/>
<OutlinedTextField
onChangeText={text => set_input(1, text)}
value={inputs[1]}
error={error[1]}
/>
<OutlinedTextField
onChangeText={text => set_input(2, text)}
value={inputs[2]}
error={error[2]}
/>
<OutlinedTextField
onChangeText={text => set_input(3, text)}
value={inputs[3]}
error={error[3]}
/>
<OutlinedTextField
onChangeText={text => set_input(4, text)}
value={inputs[4]}
error={error[4]}
/>
<OutlinedTextField
onChangeText={text => set_input(5, text)}
value={inputs[5]}
error={error[5]}
onSubmitEditing={verify}
/>
<Button onPress={verify} title='Verify' />
</View>
);
}
and it doesn't work. To be clear, the console.log() in the set_error() does print the out as I expected. It adds all the values to the array and prints out the complete array. But then, the state in the elements doesn't change. I strongly believe that this has got something to do with React's way of handling hooks rather than a bug in the <OutlinedTextField /> or something. That's why I'm leaving this here.
If such an approach is impossible with React, please suggest another way to efficiently write code to declare and use these many textfields without declaring all these error hooks.
To fix this change set_error_arr(temp); to set_error_arr([...temp]);.
The reason React does not trigger a re-render when you write set_error_arr(temp); is because of how JS arrays work. temp is holding a reference to the array. This means, that even though the values may be changing the reference has not. Since, the reference has not changed React does not acknowledge a change has occurred to your array. By writing [...temp] you are creating a new array (new reference to point too) thus React acknowledges a change occurred and will trigger a re-render.
This will also occur when working with JS objects
It's because React doesn't think that the Array has changed because it is pointing to the same reference. The content of the array itself has changed, but React only checks if it is the same Array, not the content.
There are two different solution, the first one is to create a new Array with the same content like:
const set_error = (index, err) => {
let temp = [...error];
temp[index] = err;
set_error_arr(temp);
// this logs the array correctly after running the loop each time
console.log(`This was set as error: ${error}`);
};
Or you can checkout the useReducer hook which might be more aligned to what you're trying to implement
I have a React Native Flatlist that only re-renders when its data has changed.
I give it the following data (as prop):
const posts = [
{
...post1Data
},
{
...post2Data
},
{
...post3Data
},
{
...post4Data
},
{
...post5Data
},
]
And here is my FlatList renderItem:
const renderItem = useCallback(({ item, index }) => {
const { id, userData, images, dimensions, text } = item;
return (
<View
onLayout={(event) => {
itemHeights.current[index] = event.nativeEvent.layout.height;
}}
>
<Card
id={id}
cached={false}
userData={userData}
images={images}
dimensions={dimensions}
text={text}
/>
</View>
);
}, []);
How can I add an AdMob ad between the FlatList data with a probability of 5% without skiping any data in the posts array?
I have tried this:
const renderItem = useCallback(({ item, index }) => {
const { id, userData, images, dimensions, text } = item;
if (Math.random() < 0.05) return <Ad ... />
return (
<View
onLayout={(event) => {
itemHeights.current[index] = event.nativeEvent.layout.height;
}}
>
<Card
id={id}
cached={false}
userData={userData}
images={images}
dimensions={dimensions}
text={text}
/>
</View>
);
}, []);
But this causes 2 problems:
Some items from data are skipped (not returned)
When the flatlist re-renders (because of some of its props changes) the ads might disappear (there is a chance of 95%).
Any ideas? Should I render the ads randomly in the footer of my Card component like this?
const Card = memo ((props) => {
...
return (
<AuthorRow ... />
<Content ... />
<SocialRow ... /> {/* Interaction buttons */}
<AdRow />
)
}, (prevProps, nextProps) => { ... });
const AdRow = memo(() => {
return <Ad ... />
}, () => true);
I am not really sure about this option, it works but it could violate the admob regulations (?) (because I am adapting the ad to the layout of my card component)
I would appreciate any kind of guidance/help. Thank you.
I'm not sure if you ever found a solution to this problem, but I accomplished this by injecting "dummy" items into the data set, then wrapping the renderItem component with a component that switches based on the type of each item.
Assuming your flatlist is declared like this:
<FlatList data={getData()} renderItem={renderItem}/>
And your data set is loaded into a variable called sourceData that is tied to state. Let's assume one entry in your sourceData array looks like this. Note the 'type' field to act as a type discriminator:
{
"id": "d96dce3a-6034-47b8-aa45-52b8d2fdc32f",
"name": "Joe Smith",
"type": "person"
}
Then you could declare a function like this:
const getData = React.useCallback(() => {
let outData = [];
outData.push(...sourceData);
// Inject ads into array
for (let i = 4; i < outData.length; i += 5)
{
outData.splice(i, 0, {type:"ad"});
}
return outData;
}, [sourceData]);
... which will inject ads into the data array between every 4th item, beginning at the 5th item. (Since we're pushing new data into the array, i += 5 means an ad will be placed between every 4th item. And let i = 4 means our first ad will show after the 5th item in our list)
Finally, switch between item types when you render:
const renderItem = ({ item }) => (
item.type === 'ad'
?
<AdComponent ...props/>
:
<DataComponent ...props/>
);
Introduction
I am having serious performance issues with my VirtualizedList (infinite scroll)... I have been following this to improve it but still working really slow.
When I scroll down, the UI threads go from 60fps to 30fps, and the JS thread from 60fps to 10/20fps (sometimes to 0fps). On iOS (iPhone 6) it seems to go smoothy than on my Android (Samsung Galaxy S8+)
My VirtualizedList is rendering items with different heights, and it fetches the data when its end is reached. My item extends from React.PureComponent, as it is recommended:
VirtualizedList: You have a large list that is slow to update - make sure your renderItem function renders components that follow React performance best practices like PureComponent, shouldComponentUpdate, etc. Object {
"contentLength": 20720,
"dt": 532,
"prevDt": 2933,
}
This issue comes when I scroll down around 20 items...
Code
Here is the code of the VirtualizedList:
const keyExtractor = ({ id }) => id;
...
getItem = (data, index) => data[index];
getItemCount = (data) => data.length;
renderItem = ({ item, index }) => {
const {
images,
dimensions,
description,
location,
likes,
comments,
date,
} = item;
return (
<Card
images={images}
postDimensions={dimensions}
description={description}
location={location}
likes={likes}
comments={comments}
date={date}
/>
);
};
<VirtualizedList
data={data}
getItem={this.getItem}
getItemCount={this.getItemCount}
keyExtractor={keyExtractor}
listKey={listKey}
legacyImplementation={false}
numColumns={1}
renderItem={this.renderItem}
initialNumToRender={MAX_POSTS_TO_RETRIEVE_LENGTH} // 10
windowSize={32}
maxToRenderPerBatch={15}
updateCellsBatchingPeriod={50}
removeClippedSubviews={false}
ListFooterComponent={this.renderFooter}
ListEmptyComponent={ListEmptyComponent}
onEndReached={onEndReached}
onEndReachedThreshold={0.5}
/>
Pd: each item I am rendering has an unique id.
What I have tried
I have tried to implement my own getItemLayout method for the List but for some reason it doesn't work properly.
Warning: Failed frame type: The frame frame.length is marked as required in VirtualizedList.getItemLayout, but its value is undefined.
itemHeights = [];
getItemLayout = (data, index) => {
const length = this.itemHeights[index];
const offset = this.itemHeights.slice(0,index).reduce((a, c) => a + c, 0)
return {length, offset, index}
}
renderItem = ({ item, index }) => {
const {
images,
dimensions,
description,
location,
likes,
comments,
date,
} = item;
return (
<View onLayout={(event) => this.itemHeights[index] = event.nativeEvent.layout.height}>
<Card
images={images}
postDimensions={dimensions}
description={description}
location={location}
likes={likes}
comments={comments}
date={date}
/>
</View>
);
};
<VirtualizedList
data={data}
getItem={this.getItem}
getItemCount={this.getItemCount}
getItemLayout={this.getItemLayout}
keyExtractor={keyExtractor}
listKey={listKey}
legacyImplementation={false}
numColumns={1}
renderItem={this.renderItem}
initialNumToRender={MAX_POSTS_TO_RETRIEVE_LENGTH} // 10
windowSize={32}
maxToRenderPerBatch={15}
updateCellsBatchingPeriod={50}
removeClippedSubviews={false}
ListFooterComponent={this.renderFooter}
ListEmptyComponent={ListEmptyComponent}
onEndReached={onEndReached}
onEndReachedThreshold={0.5}
/>
Any ideas? I have seen that it is a common problem but I haven't found any solution.
UPDATE
To solve the issue I was getting with getItemLayout just do:
getItemLayout = (data, index) => {
const length = this.itemHeights[index] || 0; // <----- if undefined return 0
const offset = this.itemHeights.slice(0,index).reduce((a, c) => a + c, 0)
return {length, offset, index}
}
If this happen to someone, just use my custom getItemLayout function and follow these steps
Using those values has worked for me.
initialNumToRender={10}
windowSize={5}
maxToRenderPerBatch={5}
updateCellsBatchingPeriod={30}
removeClippedSubviews={false}
...
onEndReachedThreshold={0.1}
Pd: my viewport is covered with 1.5 items
Im new in ReactNative and I'm trying to take some data from here https://www.dystans.org/route.json?stops=Hamburg|Berlin
When I try console.log results it return full API response. I dont know why in first results.distance works and return distance, but when I'm trying to do it inside FlatList nothing is returned. Sometimes it works when i want to return only item.distance but can't somethnig like <Text>{item.stops[0].nearByCities[0].city}</Text> nowhere in my code also in console. Im getting error:
undefined is not an object (evaluating 'results.stops[0]')
imports...
const NewOrContinueScreen = ({ navigation }) => {
const [searchApi, results, errorMessage] = useDystans();
console.log(results.distance);
return (
<SafeAreaView forceInset={{ top: "always" }}>
<Text h3 style={styles.text}>
Distance: {results.distance}
</Text>
<Spacer />
<FlatList
extraData={true}
data={results}
renderItem={({ item }) => (
<Text>{item.distance}</Text>
// <Text>{item.stops[0].nearByCities[0].city}</Text>
)}
keyExtractor={item => item.distance}
/>
<Spacer />
</SafeAreaView>
);
};
const styles = StyleSheet.create({});
export default NewOrContinueScreen;
And here is my hook code:
import { useEffect, useState } from "react";
import dystans from "../api/dystans";
export default () => {
const [results, setResults] = useState([]);
const [errorMessage, setErrorMessage] = useState("");
const searchApi = async () => {
try {
const response = await dystans.get("route.json?stops=Hamburg|Berlin", {});
setResults(response.data);
} catch (err) {
setErrorMessage("Something went wrong with useDystans");
}
};
useEffect(() => {
searchApi();
}, []);
return [searchApi, results, errorMessage];
};
As the name implies, FlatList is designed to render a list. Your API endpoint returns a JSON Object, not an Array, so there's nothing for the FlatList to iterate. If you want to show all the stops in the list, try passing in the stops list directly.
<FlatList
data={results.stops}
renderItem={({ item }) => (<Text>{item.nearByCities[0].city}</Text>)}
/>
Some side notes: (1) The extraData parameter is used to indicate if the list should re-render when a variable other than data changes. I don't think you need it here at all, but even if you did, passing in true wouldn't have any effect, you need to pass it the name(s) of the variable(s). (2) The keyExtractor parameter is used to key the rendered items from a field inside of them. The stop objects from the API don't have a member called distance so what you had there won't work. From my quick look at the API response, I didn't see any unique IDs for the stops, so you're probably better off letting React key them from the index automatically.