What the code does:
I made some code to make a compass/arrow rotate and point into the direction of a given coordinate.
My problem is:
When I move around and the location is updated, the TouchableOpacity in Item stops registering any kind of presses.
Tried solutions:
I’ve tried to use React.memo so that Item doesn’t uselessly re-render unless the props change, but that didn’t fix it. Should I use memo on renderItem too?
At first I was using onPress instead of onPressIn. I changed it to onPressIn That made it a bit better, because onPress isn’t called when the press is cancelled, but that did not fix it either.
Here’s my code, let me know if you guys can figure something out:
import {
Alert,
Animated,
Easing,
FlatList,
Linking,
StyleSheet,
Text,
TouchableOpacity,
View,
} from "react-native";
import React, { useEffect, memo, useRef, useState } from "react";
import * as Location from "expo-location";
import * as geolib from "geolib";
import { COLORS } from "../../assets/Colors/Colors";
export default function DateFinder() {
const [hasForegroundPermissions, setHasForegroundPermissions] =
useState(null);
const [userLocation, setUserLocation] = useState(null);
const [userHeading, setUserHeading] = useState(null);
const [angle, setAngle] = useState(0);
const rotation = useRef(new Animated.Value(0)).current;
const [selectedId, setSelectedId] = useState();
useEffect(() => {
const AccessLocation = async () => {
function appSettings() {
console.warn("Open settigs pressed");
if (Platform.OS === "ios") {
Linking.openURL("app-settings:");
} else RNAndroidOpenSettings.appDetailsSettings();
}
const appSettingsALert = () => {
Alert.alert(
"Allow Wassupp to Use your Location",
"Open your app settings to allow Wassupp to access your current position. Without it, you won't be able to use the love compass",
[
{
text: "Cancel",
onPress: () => console.warn("Cancel pressed"),
},
{ text: "Open settings", onPress: appSettings },
]
);
};
const foregroundPermissions =
await Location.requestForegroundPermissionsAsync();
if (
foregroundPermissions.canAskAgain == false ||
foregroundPermissions.status == "denied"
) {
appSettingsALert();
}
setHasForegroundPermissions(foregroundPermissions.status === "granted");
if (foregroundPermissions.status == "granted") {
const location = await Location.watchPositionAsync(
{
accuracy: Location.Accuracy.BestForNavigation,
distanceInterval: 0,
},
(location) => {
setUserLocation(location);
}
);
const heading = await Location.watchHeadingAsync((heading) => {
setUserHeading(heading.trueHeading);
});
}
};
AccessLocation().catch(console.error);
}, []);
useEffect(() => {
const rotateImage = (angle) => {
Animated.timing(rotation, {
toValue: angle,
duration: 300,
easing: Easing.linear,
useNativeDriver: true,
}).start();
};
const getBearing = () => {
const bearing = geolib.getGreatCircleBearing(
{
latitude: userLocation.coords.latitude,
longitude: userLocation.coords.longitude,
},
{
latitude: 45.472748,
longitude: -73.862076,
}
);
return bearing;
};
const checkHeading = setTimeout(() => {
if (userLocation) {
let newAngle = getBearing() - userHeading;
let delta = newAngle - angle;
while (delta > 180 || delta < -180) {
if (delta > 180) {
newAngle -= 360;
} else if (delta < -180) {
newAngle += 360;
}
delta = newAngle - angle;
}
if (delta > 5 || delta < -5) {
setAngle(newAngle);
rotateImage(newAngle);
}
}
}, 0);
return () => clearTimeout(checkHeading);
}, [userHeading]);
const textPosition = JSON.stringify(userLocation);
const DATA = [
{
id: "bd7acbea-c1b1-46c2-aed5-3ad53abb28ba",
title: "First Item",
},
{
id: "3ac68afc-c605-48d3-a4f8-fbd91aa97f63",
title: "Second Item",
},
{
id: "58694a0f-3da1-471f-bd96-145571e29d72",
title: "Third Item",
},
];
const Item = ({ item, onPress, backgroundColor, textColor }) => (
<TouchableOpacity
onPressIn={onPress}
style={[styles.item, { backgroundColor }]}
>
<Text style={[styles.title, { color: textColor }]}>{item.title}</Text>
</TouchableOpacity>
);
const renderItem = ({ item }) => {
const backgroundColor = item.id === selectedId ? "#6e3b6e" : "#f9c2ff";
const color = item.id === selectedId ? "white" : "black";
return (
<Item
item={item}
onPress={() => {
setSelectedId(item.id);
console.warn("bob");
}}
backgroundColor={backgroundColor}
textColor={color}
/>
);
};
return (
<View style={styles.background}>
<Text>{textPosition}</Text>
<Animated.Image
source={require("../../assets/Compass/Arrow_up.png")}
style={[
styles.image,
{
transform: [
{
rotate: rotation.interpolate({
inputRange: [0, 360],
outputRange: ["0deg", "360deg"],
//extrapolate: "clamp",
}),
},
],
},
]}
/>
<FlatList
data={DATA}
extraData={selectedId}
horizontal={true}
keyExtractor={(item) => item.id}
renderItem={renderItem}
style={styles.flatList}
></FlatList>
</View>
);
}
const styles = StyleSheet.create({
background: {
backgroundColor: COLORS.background_Pale,
flex: 1,
// justifyContent: "flex-start",
//alignItems: "center",
},
image: {
flex: 1,
// height: null,
// width: null,
//alignItems: "center",
},
flatList: {
backgroundColor: COLORS.background_Pale,
},
});
2
Answers
To solve my issue, I used the useMemo() hook, so that I memoize the Flat list and avoid having it re-render everytime the parent component is re-rendered. The Flat list will only re-render when the value of selectedId has changed.
The problem here is every time your location changes the Flat list also re-renders since it also rendered in the same component as the top component.
to resolve this you can use useMemo, useCallback hooks to stop the re-render os the Flat list component and only re-render it when required
or you can extract the heavy component to a separate component so that the Flat list wont re-render when the heavy component re-renders
Working Eg: Snack Example
Eg: