React Native onPress gets called on every component rendered by mapping json - javascript

I tried Googling this but due to the English language not being my greatest skill I find it hard to Google the right thing. I've tried to fix this problem myself which works, but I wonder if there are better ways to do this.
Situation:
I have a json object and want to render the values in it, so I mapped the values and render it in a component. I only have to call that component once and it will render itself multiple times acoording to how many objects are in my json object.
Now when I fire the onPress function on a single, specific rendered instance, it will fire for every rendered instance.
Desired situation:
When I have multiple rendered instances of my component, and I fire onPress on a single one, it should only fire for that instance.
Code:
I'm performing a get request and response returns a json object:
{
"objects": [
{
"name": "Alarm chauffeurs#ON=100",
"object_id": 1,
"input_value": 0,
"last_changed": "2019-03-08T14:30:54",
}, ...
Next I map the values to my component:
let cards = this.state.dataSource.objects.map((val, key) => {
return (
<Animated.View key={key} style={[cardStyle.container, { height: this.state.collapse[parseInt(val.object_id)] }]}>
<TouchableHighlight onPress={() => this.toggle(val.object_id)} />
</Animated.View>
);
});
Now I have to call {cards} in my MainView only once and React Native will render multiple cards:
return (
<View style={style.container}>
<View style={style.colDash}>
<ScrollView>
{cards}
</ScrollView>
</View>
</View>
);
Here's an image of the output
Now once I press a button, onPress gets fired on every component on screen.
I've tried:
Created an array and pushed a random value to it as id always starts at 1. The rest is done with a simple for loop. This probably isn't the best way to handle my problem.
toggle(id) {
var test = []
test.push(123);
for (var i = 1; i < 8; i++) {
if (id == i) {
var temp = new Animated.Value(75);
Animated.timing(
temp, {
toValue: 150,
duration: 500,
}
).start()
test.push(temp);
}
else {
var temp = new Animated.Value(75);
Animated.timing(
temp, {
toValue: 75,
duration: 1,
}
).start()
test.push(temp);
}
}
this.setState({
collapse: test
});
Later on, I also want to add the possibility to be able to toggle multiple instances at the same time. As of now when I fire one onPress, the rest gets reset.

Add an index to your TouchableHighlight component and try then

Related

Trouble with React Native and Firebase RTDB

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?

How do I prevent new network requests made for already-retrieved images on tab change, form change etc. in React/React Native

When I change something on my page such as checking radio buttons or switching tabs, new network requests to retrieve images are sent from the browser. I've noticed this with a couple of websites I've made, but there should never be another request; I'm not performing a fetch on changing these values in the frontend. I don't see why the images should be requested again.
I've attached a gif showing it happening in an app I'm making with React Native, although I've seen it in my React projects too. You can see the images flicker as I switch tabs and the network calls in devtools on the right, and I'm also worried about the performance impact.
How can I prevent this from happening?
For context the data flow in my app is as follows:
In App.tsx render MainStackNavigator component.
In MainStackNavigator call firebase to retrieve data (including images). Store that data in Context.
In Home.tsx render the Tabs component, but also create an array containing the tabs data, namely the name of the component to render and the component itself.
In tabs render content based on selected tab.
Home.tsx
export const Home = (): ReactNode => {
const scenes = [
{
key: "first",
component: EnglishBreakfastHome,
},
{
key: "second",
component: SecondRoute,
},
];
return (
<View flex={1}>
<Tabs scenes={scenes} />
</View>
);
};
Tabs.tsx
export const Tabs = ({ scenes }: TabsProps): ReactNode => {
const renderScenes = useMemo(() =>
scenes.reduce((map, scene) => {
map[scene.key] = scene.component;
return map;
}, {})
);
const renderScene = SceneMap(renderScenes);
const [index, setIndex] = useState(0);
const [routes] = useState([
{ key: "first", title: "Breakfast" },
{ key: "second", title: "Herbal" },
]);
const renderTabBar = ({ navigationState, position }: TabViewProps) => {
const inputRange = navigationState.routes.map((_, i) => i);
return (
<Box flexDirection="row">
{navigationState.routes.map((route, i) => {
const opacity = position.interpolate({
inputRange,
outputRange: inputRange.map((inputIndex) =>
inputIndex === i ? 1 : 0.5
),
});
return (
// Tab boxes and styling
);
})}
</Box>
);
};
return (
<TabView // using react-native-tab-view
style={{ flexBasis: 0 }}
navigationState={{ index, routes }}
renderScene={renderScene}
renderTabBar={renderTabBar}
onIndexChange={setIndex}
initialLayout={{ width: layout.width }}
/>
);
};
Look into something like react-native-fast-image. It is even recommended by the react-native docs.
It handles cashing of images. From the docs:
const YourImage = () => (
<FastImage
style={{ width: 200, height: 200 }}
source={{
uri: 'https://unsplash.it/400/400?image=1',
headers: { Authorization: 'someAuthToken' },
priority: FastImage.priority.normal,
}}
resizeMode={FastImage.resizeMode.contain}
/>
)
The problem here wasn't image caching at all, it was state management.
I made a call to a db to retrieve images, and then stored that array of images in AppContext which wrapped the whole app. Then, everytime I changed state at all, I ran into this problem because the app was being re-rendered. There were two things I did to remove this problem:
Separate Context into separate stores rather than just using a single global state object. I created a ContentContext that contained all the images so that state was kept separate from other state changes and, therefore, re-renders weren't triggered.
I made use of useMemo, and re-wrote the cards (that you can see in the image) to only change when the main images data array changed. This means that, regardless of changes to other state variables, that component will not need to re-render until the images array changes.
Either of the two solutions should work on their own, but I used both just to be safe.

Draw view with map in React Native

What I did here so far is, just created three random views and with the help of the state, I just hide/show the view. However, according to requirements that I have, the logic is the following:
If you click to any close icon (x) it should disappear and in place of this, the view which is standing in the bottom should replace it. Like in Gmail, if you archive message it will be archived and will disappear. The view in the bottom will fill that place.
Can anyone share, small example in order to figure out concept. I understand here that task should be done with array and view should be displayed with a help of map=>. I am bit in confusion since this task requirement concept is brand new to me.
I created for you a small example in which we display cards and when you click on an item, that item is removed.
The code and the working example can be found in the followig link
const App = () => {
const items = ['Item1', 'Item2', 'Item3']
const [visible, setVisible] = useState({
item0: true,
item1: true,
item2: true
});
const itemClicked = (item) => {
setVisible({
...visible,
[item]: false
})
}
return (
<View style={styles.container}>
{
items.map((item, i) => {
const itemRendered = `item${i}`;
return (
<View>
{
visible[itemRendered] &&
<TouchableOpacity onPress={() => itemClicked(itemRendered)} style={styles[itemRendered]}>
<Text>{item}</Text>
</TouchableOpacity>
}
</View>
)
})
}
</View>
);
}
I used the map to render 3 clickable items. Those will be displayed only if they should be displayed based on the value of visible[itemRendered]. The component keeps the state of which items should be visible or not. In case you click on an item, the state is updated so as the clicked item wont be displayed.

React Native: FlatList filters with extraData won’t refresh

I have a Flatlist which works like a To Do list with a filter for “ToDo” and “Upcoming”. When a user swipes to complete the item, it gets hidden from the list by changing a displayIndex attribute. I would like this to reload the list after the swipe or before the user selects “Upcoming”. After reading through other stack overflow answers I have tried adding extraData={this.state} (and creating a this.state.refresh property which changes after every swipe) to the Flatlist and I also ensured that the list items themselves are React.Components and not PureComponents. I have also tried two ways to hide the ListItems, conditionally rendering them and conditionally changing the style to hidden. Still, I am not seeing any change in my Flatlist.
Below is some partial code to see if there are any gotchas I missed:
In the MainScreen.js
async _addCompletion(myItem) {
//Lots of business logic and after it's done the below code activates
await AsyncStorage.setItem(myItem.key, JSON.stringify(myItem));
await this._updateData();
this.setState({ refresh: !this.state.refresh });
}
render() {
const buttons = ['To Do', 'Upcoming'];
const { displayModeIndex } = this.state;
return (
<View>
<ButtonGroup
onPress={this._updateButtonIndex}
buttons={buttons}
selectedIndex={displayModeIndex}
/>
<FlatList
displayMode={this.state.displayModeIndex}
data={this.state.data}
extraData={this.state}
scrollEnabled={this.state.scrollEnabled}
renderItem={({ item }) => (
<MyListItem
myListItem={item}
addCompletion={this._addCompletion}
displayIndex={this.state.displayModeIndex}
setScrollEnabled={this._setScrollEnabled}
navigation={this.props.navigation}
/>
)}
/>
</View>
);
}
In MyListItem.js
_displayMyItem {
//Logic that determines whether to display a myItem based on several factors. I can confirm this works after refreshing.
}
_hideMyItem = () => {
Animated.timing(this.containerHeight, {
toValue: 0,
}).start(() => {
this.setState({ hidden: true });
});
};
render () {
const {myItem} = this.state;
//Other code that determines how the list item looks depending on myItem data.
return (
//I have also tried to return null if this._displayMyItem(this.state.myItem) returns false
<View style={!this._displayMyItem(this.state.myItem) && { display: 'none' }}>
<Swipeable
onPress={this._onPressRow}
setScrollEnabled={this.props.setScrollEnabled}
addCompletion={this.props.addCompletion}
hideMyItem={this._hideMyItem}
myItem={this.state.myItem}
>
//Other JSX Code
</View>
)
}
The Swipeable is a custom component that calls addCompletion after a swipe and _hideMyItem after everything is done. It is not a PureComponent either.
There's a lot going on here, so I've only included code that seems relevant. I can add more if needed. The addCompletion method is a long
would help some captures...
When you swipe the item , it's just empty right?, if it leaves an empty space try this way of conditional rendering , idk if it would work.
in MyListItem.js
render () {
const {myItem} = this.state;
//Other code that determines how the list item looks depending on myItem data.
return (
//I have also tried to return null if this._displayMyItem(this.state.myItem) returns false
{!this.state.hidden?
<View style={!this._displayMyItem(this.state.myItem) && { display: 'none' }}>
<Swipeable
onPress={this._onPressRow}
setScrollEnabled={this.props.setScrollEnabled}
addCompletion={this.props.addCompletion}
hideMyItem={this._hideMyItem}
myItem={this.state.myItem}
>
//Other JSX Code
</View>:null}
)
}
wich checks if this.state.hidden is false , returns the component, else, returns null

Task orphaned for request in react-native – what does it mean?

I am trying to build a grid system for tiles with buttons and other actions. I forked trying with the react native playground grid images source, that you can find here. It produces the following "stacktrace" and error when adding zIndex to individual pics. Images are never portrayed.
In case you are interested this is the exact component I am using:
export default class GridLayout extends Component {
constructor () {
super()
const { width, height } = Dimensions.get('window')
this.state = {
currentScreenWidth: width,
currentScreenHeight: height
}
}
handleRotation (event) {
var layout = event.nativeEvent.layout
this.setState({ currentScreenWidth: layout.width, currentScreenHeight: layout.height })
}
calculatedSize () {
var size = this.state.currentScreenWidth / IMAGES_PER_ROW
return { width: size, height: size }
}
renderRow (images) {
return images.map((uri, i) => {
return (
<Image key={i} style={[styles.image, this.calculatedSize()]} source={{uri: uri}} />
)
})
}
renderImagesInGroupsOf (count) {
return _.chunk(IMAGE_URLS, IMAGES_PER_ROW).map((imagesForRow) => {
console.log('row being painted')
return (
<View key={uuid.v4()} style={styles.row}>
{this.renderRow(imagesForRow)}
</View>
)
})
}
render () {
return (
<ScrollView style={styles.grid} onLayout={(ev) => this.handleRotation(ev)} contentContainerStyle={styles.scrollView}>
{this.renderImagesInGroupsOf(IMAGES_PER_ROW)}
</ScrollView>
)
}
}
var styles = StyleSheet.create({
grid: {
flex: 1,
backgroundColor: 'blue'
},
row: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'flex-start',
backgroundColor: 'magenta'
},
image: {
zIndex: 2000
}
})
It may have something to do with the way you are rendering. Your request seems to get stuck. It looks like this is the code where the error is thrown in RN (from RCTImageLoader.m):
// Remove completed tasks
for (RCTNetworkTask *task in _pendingTasks.reverseObjectEnumerator) {
switch (task.status) {
case RCTNetworkTaskFinished:
[_pendingTasks removeObject:task];
_activeTasks--;
break;
case RCTNetworkTaskPending:
break;
case RCTNetworkTaskInProgress:
// Check task isn't "stuck"
if (task.requestToken == nil) {
RCTLogWarn(#"Task orphaned for request %#", task.request);
[_pendingTasks removeObject:task];
_activeTasks--;
[task cancel];
}
break;
}
}
I'm not exactly sure how to solve this, but a couple ideas to help you debug:
The <Image> component has some functions and callbacks that you could utilize to try to further track down the issue (onLoad, onLoadEnd, onLoadStart, onError, onProgress). The last two are iOS only but you could see if any of those are called to see where in the process things get hung up.
Alternatively, the way I would do this would be to use a ListView and place the image urls in the datasource prop for the ListView (utilizing the _.chunk method to group them as you already do). This would be a little cleaner way of rendering them IMO
I think this issue is possibly related to you trying to load http images in ios. I get this same error in my project when using faker images in my seed data on ios (android doesn't care) which only come back with http. Here's a better explanation that I found on this topic React-native loading image over https works while http does not work
according to react native, it better to have the item component a pure component and all the data to be rendered in the component should be in the item object itself for better performance. In case of image I also dont know how they are handling it. loading image from url might be something which is not in the item object itself. However, i am also getting the issue and I am calling a function to load an image icon which are packed with application.

Categories