React Native setState not re-render - react-native

I'm making a game for university project. I could not figure out why it's not re-render the screen when I use setState() for status, but it does when I use for fool. If I use console.log(status) It was altered after the setStatus.
const [status, setStatus] = React.useState({ astrounauts: 0, research: 0, money: 0, publicity: 0 });
const [fool, setFool] = React.useState(false);
const acceptCard = (index) => {
let statusAux = status;
statusAux.astrounauts += cards[index].acceptCardStatus.astrounauts;
statusAux.research += cards[index].acceptCardStatus.research;
statusAux.money += cards[index].acceptCardStatus.money;
statusAux.publicity += cards[index].acceptCardStatus.publicity;
//no re-render... =C
setStatus(() => statusAux);
//it does trigger re-render, not working without that.. this is dumb
setFool(() => !fool);
}
...
<View style={styles.cardsOptions}>
<Swiper
cards={cards}
renderCard={(card =>
<Card card={card} />
)}
onSwipedRight={(cardIndex) => acceptCard(cardIndex)}
onSwipedLeft={(cardIndex) => rejectCard(cardIndex)}
// onSwiped={(cardIndex) => { console.log(cardIndex) }}
onSwipedAll={() => { console.log('onSwipedAll') }}
disableBottomSwipe={true}
disableTopSwipe={true}
outputRotationRange={["-10deg", "0deg", "10deg"]}
cardIndex={0}
backgroundColor={'#18191A'}
stackSize={100}
>
</Swiper>
</View>

You are setting your state to a function. the setState callback accepts the value itself, not a callback that returns the value. You need to change:
setStatus(() => statusAux);
To:
setStatus(statusAux);
In addition to that... you are mutating the original object, you need to create a new object for React to detect the change. You can do using the spread operator
like this:
let statusAux = {...status};

Just try to do this, Don`t need to use "()=>"
const acceptCard = (index) => {
let statusAux = status;
statusAux.astrounauts += cards[index].acceptCardStatus.astrounauts;
statusAux.research += cards[index].acceptCardStatus.research;
statusAux.money += cards[index].acceptCardStatus.money;
statusAux.publicity += cards[index].acceptCardStatus.publicity;
//no re-render... =C
setStatus(statusAux);
//it does trigger re-render, not working without that.. this is dumb
setFool(!fool);
}

Related

Render Polygon only if within MapView in React Native

I have a long list of areas loaded from a SQLite database into react-native-maps. Each area has a particular status, and a fillColor that gets calculated based on its status. As some areas end up having hundreds of Polygons, the maps gets slower the more areas are calculated. Is there a way to detect if a Polygon is within the MapView to hide it when is not within the observable map?
So far I have a memoized component that gets the coordinates and a couple of helper function to get the color based on the area. The looped component gets then injected inside the MapView.
const Areas = useMemo(
() =>
areas &&
areas.map((area: Area) => {
console.log('rendering');
const coordinates = getCoordinates(area.geometry);
const areaColor = getAreaColor(area.status);
return (
<View key={area.CODE}>
<Polyline
strokeWidth={2}
fillColor={'transparent'}
coordinates={coordinates}
strokeColor={areaColor}
tappable={true}
onPress={() => handlePressArea(area)}
/>
</View>
);
}),
[areas]
);
const getCoordinates = (coords: string): LatLng[] => {
const geometry = JSON.parse(coords);
const output = geometry[0].map((item: Point[]) => ({
latitude: item[1],
longitude: item[0],
}));
return output;
};
const getAreaColor = (status: Area['status']): string => {
if (status === STATUS.ONE) {
return 'rgba(54, 155, 247, 0.6)';
} else if (status === STATUS.TWO) {
return 'rgba(240, 54, 0, 0.6)';
} else {
return 'rgba(235, 149, 50, 0.6)';
}
};
Eventually I mount the component like this:
<MapView
initialRegion={region}
provider={PROVIDER_DEFAULT}
rotateEnabled={false}
style={styles.map}
showsUserLocation={true}
onRegionChangeComplete={handleRegionChange}
customMapStyle={mapStyle}
>
{AreasView}
</MapView>
I tried to get a ref for each Polygon but the number of callbacks is so high that makes the app crash. I also tried with ObservableAPI but with no success. Simplifying the polygons is not an option either. Is there any way to detect if each polygon is within the observable map to hide them when out of view? I been stuck for some days so far, I hope someone could help.
Thank you!
UPDATE
I have implemented a function that checks if a polygon is within boundaries on region change as such:
<MapView
...props
ref={mapRef}
onRegionChangeComplete={getMapBounds}
/>
const mapRef = useRef<MapView>(null);
const getMapBounds = async () => {
await mapRef?.current?.getMapBoundaries().then(res => setMapBounds(res));
};
This basically stores inside the state the northEast and southWest boundaries of the map every time the user changes view.
Then to check if is in view:
const isInView = useMemo(
() => (point: LatLng) => {
if (mapBounds) {
const { northEast, southWest } = mapBounds;
const x = point.longitude;
const y = point.latitude;
return (
northEast.latitude >= y &&
y >= southWest.latitude &&
northEast.longitude >= x &&
x >= southWest.longitude
);
}
},
[mapBounds]
);
Gets a portion of the polygon and checks is the latitude and longitude of the point are within those boundaries
And so this leaves to:
const AreasView = useMemo(
() =>
areas &&
areas.map((area: Area) => {
const coordinates = getCoordinates(area.geometry);
const areaColor = getAreaColor(area.status);
const isVisible = isInView(coordinates[0]);
if (isVisible) {
return (
<View key={area.CODE}>
<Polygon
strokeWidth={2}
fillColor={areaColor}
coordinates={coordinates}
strokeColor={areaColor}
tappable={true}
onPress={() => handlePressArea(area)}
/>
</View>
);
}
}),
[areas, isInView]
);
It's still quite slow...shall I use useCallback instead, or is it any function for any library that is more optimised than that?
Cheers

What is the best implementation of pagination using Apollo hooks in a react native flatlist?

Im looking for insight on how best to implement a loadMore function in the onEndReached callback provided by flatlist while using apollo hooks! I've got it sort of working except every time i load more results the list jumps to the top since the data field of flatlist relies on incoming data from useQuery that changes every time it asks for more...
I dont know if i should be implementing offset and limit based pagination, cursor based, or some other strategy.
If anyone has tips that would be huge! thanks!
I am using Shopify storefront graphql queries to get product list, and here is how I have implemented pagination using cursor-based pagination method on FlatList. Hope you find something usable.
First, declare two variables which will be used later to check whether the Flatlist scrolled and reached on the end.
// declare these two variables
let isFlatlistScrolled = false;
let onEndReachedCalledDuringMomentum = false;
Now, create a method called handleFlatlistScroll which will be used to changed the values of the variable isFlatlistScrolled when the flatlist is scrolled.
const handleFlatlistScroll = () => {
isFlatlistScrolled = true;
};
Also declare a method to change the value of onEndReachedCalledDuringMomentum.
const onMomentumScrollBegin = () => {
onEndReachedCalledDuringMomentum = false;
}
Now, create your flatlist like this :
return (
<Layout style={{ flex: 1 }}>
<Query
query={GET_PRODUCT_LIST_BY_COLLECTION_HANDLE}
variables={{
handle: props.route.params.handle,
cursor: null,
}}>
{({
loading,
error,
data,
fetchMore,
networkStatus,
refetch,
stopPolling,
}) => {
if (loading) {
return <ProductListPlaceholder />;
}
if (data && data.collectionByHandle?.products?.edges?.length > 0) {
stopPolling();
return (
<FlatList
data={data.collectionByHandle.products.edges}
keyExtractor={(item, index) => item.node.id}
renderItem={renderProductsItem}
initialNumToRender={20}
onScroll={handleFlatlistScroll}
onEndReached={() => {
if (
!onEndReachedCalledDuringMomentum &&
isFlatlistScrolled &&
!isLoadingMoreProducts &&
!loading &&
data.collectionByHandle?.products?.pageInfo?.hasNextPage
) {
onEndReachedCalledDuringMomentum = true;
setLoadingMoreProductsStatus(true);
// your loadmore function to fetch more products
}
}}
onEndReachedThreshold={Platform.OS === 'ios' ? 0 : 0.1}
onMomentumScrollBegin={onMomentumScrollBegin}
// ... your other flatlist props
/>
);
}
return <EmptyProductList />;
}}
</Query>
</Layout>
)
As you can see in above code, load more function only called when flatlist is properly scrolled at the end.

React Native State Management Question - when does useState hook load?

I have a FlatList of items that has a "remove" button next to it.
When I click the remove button, I am able to remove the item from the backend BUT the actual list item is not removed from the view.
I am using useState hooks and it was to my understanding that the component re-renders after setState happens.
The setState function is used to update the state. It accepts a new
state value and enqueues a re-render of the component.
https://reactjs.org/docs/hooks-reference.html
What am I missing with how state is set and rendering?
I don't want to use the useEffect listener for various reasons. I want the component to re-render when the locations state is updated....which I am pretty sure is happening with my other setStates....not sure if I am totally missing the mark on what setState has been doing or if it's something specific about setLocations().
const [locations, setLocations] = useState(state.infoData.locations);
const [locationsNames, setLocationsNames] = useState(state.infoData.names]);
...
const removeLocationItemFromList = (item) => {
var newLocationsArray = locations;
var newLocationNameArray = locationsNames;
for(l in locations){
if(locations[l].name == item){
newLocationsArray.splice(l, 1);
newLocationNameArray.splice(l, 1);
} else {
console.log('false');
}
}
setLocationsNames(newLocationNameArray);
setLocations(newLocationsArray);
};
...
<FlatList style={{borderColor: 'black', fontSize: 16}}
data={locationNames}
renderItem={({ item }) =>
<LocationItem
onRemove={() => removeLocationItemFromList(item)}
title={item}/> }
keyExtractor={item => item}/>
UPDATED LOOP
const removeLocationItemFromList = (item) => {
var spliceNewLocationArray =locations;
var spliceNewLocationNameArray = locationsNames;
for(f in spliceNewLocationArray){
if(spliceNewLocationArray[f].name == item){
spliceNewLocationArray.splice(f, 1);
} else {
console.log('false');
}
}
for(f in spliceNewLocationNameArray){
if(spliceNewLocationNameArray[f] == item){
spliceNewLocationNameArray.splice(f, 1);
} else {
console.log('false');
}
}
var thirdTimesACharmName = spliceNewLocationNameArray;
var thirdTimesACharmLoc = spliceNewLocationArray;
console.log('thirdTimesACharmName:: ' + thirdTimesACharmName + ', thirdTimesACharmLoc::: ' + JSON.stringify(thirdTimesACharmLoc)); // I can see from this log that the data is correct
setLocationsNames(thirdTimesACharmName);
setLocations(thirdTimesACharmLoc);
};
This comes down to mutating the same locations array and calling setState with the same array again, which means that the FlatList which is a pure component will not re-render since the identity of locations has not changed. You could copy the locations array to newLocationsArray first (similarly with the newLocationNameArray) to avoid this.
var newLocationsArray = locations.slice();
var newLocationNameArray = locationsNames.slice();

How to get a component from an array and change its props in react native?

I've created some components in a for loop and push them to an array. Can i get any component from this array to change its props -like title ?
fields = [];
for (let i = 0; i < assets.fieldNames[assets.systemLang].length; i++) {
fields.push(
<InfoField
handlePress={() => this.fieldPressed(i)}
key={i}
title={assets.fieldNames[assets.systemLang][i]}
value="" />
);
};
.....
<View style={styles.infoFields}>
{
fields
}
</View>
and i have a function that i need something like this
changeComponentTitle = () => {
fields[indexForComponent].props.title = "new Title"
}
You'll need to make sure that you trigger a re-render in some way after you make the update (perhaps by putting fields in a state variable and calling setState as mentioned in the comments) but you can use cloneElement to essentially update a single prop of an existing element: https://reactjs.org/docs/react-api.html#cloneelement
changeComponentTitle = () => {
fields[indexForComponent] = React.cloneElement(fields[indexForComponent], {title: "new Title"});
}

Dynamic build component with AsyncStorage

I am trying to build my sub-component in function _buildComponent, and put result into render(), just have a look at my code below
the problem I met was the AsyncStorage.getItem() is running async, causing it render nothing there in render() method
...react
_buildComponent = async (key) => {
let val = await AsyncStorage.getItem(key)
console.log(key + ' : ' +val);
debugger;//code will run to here after ScrollableTabView finish rendering. but I need to build Arr first.
if(val == 1) return <PopularView tabLabel={key}>{key}</PopularView>
}
render() {
let Arr = Constants.TABS.map(item =
return this._buildComponent(item).done();
})
debugger;//code will run into here directly without waiting building Arr above, making Arr was null when rendering ScrollableTabView
return (
<ScrollableTabView
tabBarBackgroundColor='#2196F3'
tabBarInactiveTextColor='mintcream'
tabBarTextStyle={{marginTop:27}}
initialPage={0}
renderTabBar={() => <ScrollableTabBar/>}
>
{Arr}// Arr is null here because the _buildComponent method was not finish yet.
{/* <PopularView tabLabel='Java'>Java</PopularView>
<PopularView tabLabel='IOS'>IOS</PopularView>
<PopularView tabLabel='Android'>Android</PopularView>
<PopularView tabLabel='Javascript'>Javascript</PopularView> */}
</ScrollableTabView>
)
}
...
I have explain my issue in comment, please check it, thanks guys. I do not know what's the best practise to prepare variable before running render().
Try this solution which dynamically add tabs (Updated)
_buildComponent = async () => {
let tabsData = [];
await AsyncStorage.multiGet(Constants.TABS).then(response => {
Constants.TABS.forEach((item,index) =>
{
if(response[index][1] == 1) {
tabsData.push(response[index][0]); // key
}
}
);
// This only render once
this.setState({ tabLabels: tabsData })
})
}
componentDidMount() {
this._buildComponent()
}
render() {
const tabLabelList = this.state.tabLabels.map((key) => {
return (
<PopularView tabLabel={key}>{key}</PopularView>
)
})
return (
<ScrollableTabView
tabBarBackgroundColor='#2196F3'
tabBarInactiveTextColor='mintcream'
tabBarTextStyle={{marginTop:27}}
initialPage={0}
renderTabBar={() => <ScrollableTabBar/>}
>
{tabLabelList}
</ScrollableTabView>
)
}