Why is my React Native component not re-rendering on state update? - reactjs

I'm struggling with this React-Native component for a few days now. You should probably know that React-Native is kind of new to me so... sorry if the solution is obvious to you.
I'm using react-native-maps and I have several markers on my map. Each one of them has some data stored in my state and I want the callout to display a piece of this state on press.
Here are my states :
const [markersDetails, setMarkersDetails] = useState([]);
const [activeMarker, setActiveMarker] = useState({});
My activeMarker is updated by this function :
const markerSearch = (markerId) => {
let stockMarker = markersDetails.find((singleMarker) => {
return Number(singleMarker.idMarker) === markerId;
});
console.log("Stock ", stockMarker);
setActiveMarker(stockMarker);
console.log("State ", activeMarker);
};
And this function is called, inside my return, with the onPress of any marker :
<Marker
key={Number(marker.index)}
coordinate={{
latitude: Number(marker.latitude),
longitude: Number(marker.longitude),
}}
pinColor="blue"
onPress={() => {
markerSearch(Number(marker.index));
}}
>
{activeMarker !== {} && activeMarker.markerName && (
<Callout>
<View>
<Text>{activeMarker.markerName}</Text>
</View>
</Callout>
)}
</Marker>
But whenever I press on a marker, the callout opens immediatly while my state is not yet updated. So the text within the callout refers either to the previous marker or is empty (if it's the first marker I press on).
I've checked with console.log and my state is clearly updated but it takes a little bit more time. And I don't know why my callout is not re-rendering when this state is updating.
I've tried a ton of things to make this works but I can't figure this out...

Try doing something like that:
You can extract the section to a new component
Then inside this use the useEffect hook
export default function CalloutComponent({activeMarker}) {
const [markerName, setMarkerName] = useState('')
useEffect(() => {
setMarkerName(activeMarker?.markerName)
}, [activeMarker?.markerName])
if(!!markerName) return null
return (
<Callout>
<View>
<Text>{markerName}</Text>
</View>
</Callout>
)
}
And use this new component in your Main view
<Marker
...
>
<CalloutComponent activeMarker={activeMarker}/>
</Marker>

Related

How do I stop React Native Maps' Draggable Marker from disabling touches outside the MapView on iOS using Google as the provider?

On iOS with Google provider React Native Maps' Draggable Marker disables touches outside the MapView until it registers a touch inside the MapView. On Android everything is fine, but for some reason on iOS when I finish dragging a marker on the map and onDragEnd is called, no touch events are registered unless they are on TextInputs. Occasionally a TouchableOpacity will flash momentarily, but it's onPress function is never called. However if I touch inside the MapView, even nowhere near the marker, everything goes back to the way it's supposed to be. It's like react native maps has some finishing event that doesn't occur that forces the focus to stay on the map.
There are a couple tricky things going on but I don't think they're the culprits:
I use setState with onDragStart and onDragEnd to disable the ScrollView parent component, otherwise the dragging gets interrupted by the scroll on Android.
As part of onDragEnd I make a callout with react-native-geocoding, then update the region state. I have commented all this out and it still doesn't work.
The plan is to animate to the new region after the state is updated, but until this is resolved there's no point. Here's my code, or what's left of it after taking out all the commented stuff:
const MapSection = (props) => {
const {
location, setIsDraggingMarker, onDragMarkerEnd, region,
} = props;
const [isLoading, setIsLoading] = useState(false);
const onDragEnd = (e) => {
onDragMarkerEnd(e.nativeEvent.coordinate);
};
if (!isEmpty(location)) {
return (
<View style={styles.mapContainer}>
<MapView
provider={PROVIDER_GOOGLE}
style={styles.mapView}
initialRegion={region}
scrollEnabled={false}
zoomEnabled={false}
rotateEnabled={false}
>
<Marker
draggable
onDragStart={() => setIsDraggingMarker(true)}
onDragEnd={onDragEnd}
coordinate={region}
>
<Image
style={styles.imageStyle}
resizeMode="stretch"
source={require('../../../../assets/images/draggableMarkerPin.png')}
/>
<Callout
style={styles.customCallout}
onPress={() => { }}
>
<View style={styles.callout}>
<Feather
name="move"
size={12}
color="black"
/>
<Text style={{
paddingLeft: 4,
fontSize: 14,
}}
>
Press and drag to fine tune location!
</Text>
</View>
</Callout>
</Marker>
</MapView>
</View>
);
}
return null;
};
export default MapSection;
//FROM THE PARENT COMPONENT, PASSED AS PROPS
const onDragMarkerEnd = (coords) => {
setIsDraggingMarker(false);
setRegionWithLatLng(coords.latitude, coords.longitude);
};
const setRegionWithLatLng = async (latitude, longitude) => {
const fullLocationData = await geocodeLocationByCoords(latitude, longitude);
const currentLocation = { ...fullLocationData };
const newRegion = {
...region,
latitude: fullLocationData.geometry.location.lat,
longitude: fullLocationData.geometry.location.lng,
};
setRegion(newRegion);
dispatchEventDetailsState({
type: FORM_INPUT_UPDATE, value: currentLocation, isValid: true, input: 'location',
});
};
If there's any way to just trick the MapView into thinking it's been pressed (I'ved tried referencing the MapView and calling mapView.current.props.onPress(), no dice), then I'm fine with that. Just any workaround.

Component not re-rendering in react

const { contacts, getContacts, } = useContext(
ContactContext
);
useEffect(() => {
getContacts();
//eslint-disable-next-line
});
//prettier-ignore
return (
<Fragment>
{contacts.length === 0 ? (
<h4 style={{ textAlign: 'center' }}>Please add a contact</h4>
): null}
{contacts.map(contact => (
<ContactItem contact={contact} />
))}
</Fragment>
);
};
contacts is initially an empty array and after getContacts makes the request to the server, it updates the contacts state
but somehow the words 'Please add a contact' is always showing even after getContacts() returns an array with a few contacts. it seems like it does not re-render that part of the component because when the component initially ran, contacts was an empty array
When it comes to "why will/won't my component update", React follows three simple rules. It will only "re-render" your component if:
its props change
its state changes
its context changes
For those last two it's critical that you change them correctly, using the appropriate setter method. For instance, if you are using state via hooks (vs. class-based state), ie.
const [foo, setFoo] = useState('');
You have to use setFoo:
setFoo(newValue);
If you simply change the Javascript variable:
foo = newValue;
React has no way of knowing about the change, and so your component won't re-render.
While you haven't shown all your code, it seems very likely you're changing your context directly, instead of using the appropriate setter function (as part of a state variable).
P.S. See https://reactjs.org/docs/context.html#updating-context-from-a-nested-component if you need clarification on the pattern of using state to control context.
Please use hook called useState
import { useState } from "react";
const { contacts, getContacts, } = useContext(
ContactContext
);
const [state, setState ] = useState({
contacts:[]
})
useEffect(() => {
setState({
contact:getContacts()
})
//eslint-disable-next-line
});
//prettier-ignore
return (
<Fragment>
{state.contacts.length === 0 ? (
<h4 style={{ textAlign: 'center' }}>Please add a contact</h4>
): null}
{state.contacts.map(contact => (
<ContactItem contact={contact} />
))}
</Fragment>
);
};
the only thing that worked was this, but I don't know why it did not work previously and why it does work now:
if(contacts.length === 0 ){
return <h4>Please add a contact</h4>
}
I put this line of code on top of the other return statement

setState() block the UI when two setState() run

Here is my componentDidMount() method :
componentDidMount() {
const subscription = accelerometer.subscribe(({ x, y, z, timestamp }) => {
x = Math.trunc(x*100);
this.setState({x})
});
}
In above method, every 100 millisecond state is changing. I used that state in my render() method as below :
render() {
const animatedImageStyle = StyleSheet.flatten([
styles.captureButton,
{
transform: [{rotateZ:this.state.x + 'deg'}]
}
])
return (
<SideMenu
menu={leftMenu}
isOpen={this.state.isOpenLeftMenu}
menuPosition={'left'}
bounceBackOnOverdraw={false}
onChange={(isOpenLeftMenu) => this.updateLeftMenuState(isOpenLeftMenu)}
>
<View>
<TouchableOpacity
activeOpacity={0.5}
onPress={(this.state.recordingMode == 'camera')?() => this.takePicture():() => this.toggleRecording()}
>
<Image
source={require('../assets/imgs/forRotate.png')}
style={animatedImageStyle}
/>
</TouchableOpacity>
</View>
</SideMenu>
)
}
Now, the problem is that when I trying to open sidemenu, it is not opening, I mean it opening but hanging too much. My whole app hanging too much.
I think that's because of below method :
updateLeftMenuState(isMenuOpen) {
this.setState({
isOpenLeftMenu:isMenuOpen
})
}
Notice that I am updating another state called isOpenLeftMenu, which may blocked during I update state x.
Can anyone tell me what't going wrong here ?
you can move the animation view in a separate component along with subscription logic. So the state update of that component won't affect the SideMenu component.

How to properly update/re-render a component that is not a child React Native?

I'm using a react-navigation. More specifically, I have a materialTabNavigator nested inside of a drawerNavigator. Each tab is in itself a stackNavigator. I have a button in homeScreen, that navigates to makePost.js. There I take in information and store it to Async storage using a simple wrapper.
In Posts.js there's a FlatList displaying each post as a component. The data for the FlatList is initially set correctly after making a request from Async Storage. The problem is that this only happens when the app is first opened. I have tried many different approaches to solve this. The only way so far I've found is to continuously setState in ComponentDidUpdate() in Posts.js. Obviously this is problematic, because it re-renders constantly. I can set a flag to stop is from rendering, but then it will not re-render again.
Ultimately, what I'd like to happen is that when I hit the user is done entering their information and is ready to make a post, they hit the button in makePost.js, and the data in the FlatList of Posts.js is update.
I've tried to pass parameters using navigation, does not work, parameters get lost somewhere, probably because of the nested navigators.
I could really used some guidance on the proper way to accomplish this.
( Navigators; not sure why this is forcing to one line )
---drawer
--tabNav
-home
homeScreen.js
makePost.js
-posts
posts.js
-messages
--drawer1
--drawer2
//Posts.js
export default class Posts extends React.Component {
state = {
rows: [
{id: 0, text: "dog"},
],
}
componentDidMount() {
this.loadState();
}
loadState = () => {
var value = store.get('posts').then((res => {
if (res === null) {
res = [{id: 0, text: "default"}]
} else {
res = res
}
this.setState({rows: res})
}))
}
componentDidUpdate() {
this.loadState();
}
renderItem = ({item}) => {
return (
<BoardTab style={styles.row} />
)}
render() {
return (
<View style={styles.view}>
<FlatList
ListFooterComponent={this.renderFooter}
style={styles.container}
data={this.state.rows}
renderItem={this.renderItem}
keyExtractor={extractKey}
>
</FlatList>
<BoardScreenFooter />
</View>
);
}
And Posts.js button looks like this:
<TouchableOpacity
onPress={ () => {
this._onPressButton
this.storeFunc(this.state.newPost)
const retval = this.state.rows
this.props.navigation.navigate('Board',
{rowsID: retval});
}
}>
<Icon
reverse
name='md-camera'
type='ionicon'
color='green'
size={12}
/>
</TouchableOpacity>
storeFunc(newObj) {
newObj.id = newObj.id + 1
store.push('posts', newObj)
store.get('posts').then((res) => {
this.setState({rows: res})
})
}
Rapidly, i would say: use Redux. It alloq you to have global state in your app, which mean you can access the state anywhere (And also set them anywhere)
When opening the app, you get the data from the AsyncStore into the Redux store. You listen to the redux state (Which will be a props in your component) and display your list. When modifying your list in the other tab, you need to do 2 things:
Store the new data in the AsyncStorage
Update the state in the redux store. Since Posts.js will be listening at the redux store (as a props), it will re-render each time your data will change
A simple way to re-render a React-Navigation screen view on navigating to it:
All credit goes to Andrei Pfeiffer, Jul 2018, in his article: "Handle Tab changes in React Navigation v2" https://itnext.io/handle-tab-changes-in-react-navigation-v2-faeadc2f2ffe
I will reiterate it here in case the above link goes dead.
Simply add a NavigationEvents component to your render function with the desired listener prop:
render() {
return (
<View style={styles.view}>
<NavigationEvents
onWillFocus={payload => {
console.log("will focus", payload);
this.loadState();
}}
/>
<FlatList
ListFooterComponent={this.renderFooter}
style={styles.container}
data={this.state.rows}
renderItem={this.renderItem}
keyExtractor={extractKey}
>
</FlatList>
<PostScreenFooter />
</View>
);
}

VirtualizedList: You have a large list that is slow to update

I use FlatList with large number of items. I get following alert from Expo XDE.
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. {"dt":13861,"prevDt":1498372326027,"contentLength":6624}
I used some optimization approaches to my FlatList for example PureComponent, but I still get this alert. Before I will describe my optimizations, could you tell me if this alert appears always even though FlatList is optimized? Or maybe it indicated actual issues with performance? I ask because performance of my FlatList is good.
I was previously seeing this error. After optimizing my code, I no longer see it. I figured out the problem by adding console.log() statement to the render() function of the Component that creates the FlatList, and the function that renders each item in the List. I noticed that my code was previously re-rendering the entire FlatList and all its items whenever there's a state change to any component on that page (even a component that's not related to the FlatList). I fixed this by converting various components to PureComponents. Here's what my FlatList declaration looks like:
<FlatList
ref={(ref) => { this.flatListRef = ref; }}
data={allPosts}
initialNumToRender={7}
renderItem={({ item }) =>
<Post postJson={item} isGroupAdmin={isGroupAdmin} user={user} />
}
/>
Notice that I'm returning <Post /> which is a pure component:
import React, { PureComponent } from 'react';
class Post extends PureComponent {
render() { ... }
}
This ensures that the FlatList re-renders a only if the post changes. When I was previously passing a normal function to renderItem i.e., a function that does something like this:
return (
<View>
...
</View>
);
I noticed that the FlatList was re-rendering all items whenever any item changed. Now, by using a PureComponent, the FlatList only renders the new item added to the list (if the list is already being displayed).
It still takes relative long to render the entire list the first time. However, initialNumToRender ensures that the screen is filled up pretty much instantaneously (while the remain items get rendered in the background). And more importantly, after that initial rendering, the FlatList only ever has to render one item at a time (the item that changes).
I found this post very helpful).
I've just realized this is also explained here
I noticed that the answer to this question dosen't proffer solution for those using functional component and hooks. I encountered this problem and i was able to get rid of it by using the hook "useMemo()"
<FlatList
keyExtractor={keyExtractor}
data={productsState.products}
renderItem={renderItem}
/>
const renderItem = ({ item }) => (
<ListItem
title={item.ProductName}
subtitle={(item.ProductQuantity) + " " + (item.QuantityType !==
null ? item.QuantityType : " ") }
bottomDivider
topDivider
chevron
checkmark={checkMark}
onLongPress={() => setCheckMark(!checkMark)}
rightSubtitle={(item.Currency !== null ? item.Currency: " " ) +
" " + (item.productCost !== null ? item.productCost: " " )}
rightSubtitleStyle={{ marginTop: -20 }}
badge={{ value: item.sellingPrice, textStyle: { color: 'orange' }, containerStyle: { marginTop: -20 } }}
/>
)
The renderItem function is an expensive computation, because it a long list to render. Instead I memoize it as follows
const memoizedValue = useMemo(() => renderItem, [productsState.product]);
<FlatList
keyExtractor={keyExtractor}
data={productsState.products}
renderItem={memoizedValue}
/>
const renderItem = ({ item }) => (
<ListItem
title={item.ProductName}
subtitle={(item.ProductQuantity) + " " + (item.QuantityType !==
null ? item.QuantityType : " ") }
bottomDivider
topDivider
chevron
checkmark={checkMark}
onLongPress={() => setCheckMark(!checkMark)}
rightSubtitle={(item.Currency !== null ? item.Currency: " " ) +
" " + (item.productCost !== null ? item.productCost: " " )}
rightSubtitleStyle={{ marginTop: -20 }}
badge={{ value: item.sellingPrice, textStyle: { color: 'orange' }, containerStyle: { marginTop: -20 } }}
/>
)
Don't forget to import useMemo from react, inorder to make this work.
Good Luck!
If you are using a functional component, wrapping the component in memo is a good way to prevent unnecessary renders without going through the hassle of converting a functional component to a pure class component. This post explains it more
follow this example:
In the parent component:
import React from 'react';
import {FlatList} from 'react-native';
import PostCard from './PostCard';
export const NewsFeeds = props => {
return (
<FlatList
data={data}
initialNumToRender={4}
refreshing={loading}
renderItem={_renderitem}
/>
);
};
const _renderitem = ({item}) => <PostCard item={item} />;
In the child component
import React, {memo} from 'react';
import {View} from 'react-native';
const PostCard = (props) => {
return (
<View>
</View>
);
};
export default memo(PostCard);
If you are using a class component, make sure your component is a pure component by extending React. PureComponent in your class definition
class NewsFeeds extends React.PureComponent {
render() {
return (
<FlatList
data={data}
initialNumToRender={4}
refreshing={loading}
renderItem={_renderitem}
/>
)
}
}
Adding this prop :
initialNumToRender={n}
worked for me (n being a considerably short amount, for example 5).
I figured it out, why this bug is happened. The main problem is, when your onEndReached event is happened, im sure you are loading something from server, which means, you need to wait until your loading is finished from server, so after that you can call onEndReached event.
But in your case there is multilple calling of onEndReached event. So when it happens, your application was trying to load datas from server again and again.
Ok, how to solve this problem: you need to create new state, for example
this is realization of infinite scrolling by pagination.
const [loader, setLoader] = useState<boolean>(false);
const onEndReached = (page) => {
if (next && !loader) {
setPage(page + 1)
}
}
const loadData = async () => {
setLoader(true);
const resp = await getData();
setLoader(false);
}
<FlatList ...someprops onEndReached={onEndReached} />
On top of all the answers given, you can also try setting removeClippedSubviews to true.
<FlatList
removeClippedSubviews
// ...other props
/>
By enabling removeClippedSubviews the memory is freed up when an item disappears from the view. When you have a long and complex list (i.e. a list of cards) the DOM of each card can get pretty large so it's best to free up the memory when it's not visible.
In addition if you combine with useCallback() rather than useMemo() you free up a bit more memory when your "data" changes
const renderItem = useCallback(originalRenderItem, [data])
the useMemo() approach will memoize based on the value, but it should really free itself up when the data changes. By doing useCallback() you're getting the benefit of using the "function as a parameter" so you don't need to
const renderItem = useCallback(({item, index}
=> originalRenderItem({item, index}), [data])
Thus making it look like a wrapped function without as much reading for the next person.
Doing this two:
prevents the calling the potentially expensive render() function of recently updated components.
reduces memory used by invisible components
frees up the memoized data if data changes sooner.
Also make sure, you don't encapsulate FlatList with ScrollList.
For me it accidentally appears, because I used native-base, and didn't noticed, that their Component <Content> replace ScrollList.
For more Information see here: https://stackoverflow.com/a/54512633/1256697
add memo to your renderItem component when export it
import React,{memo} from "react";
.
.
.
your code
.
.
.
export default memo(your component name);

Resources