skip to Main Content

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


  1. Chosen as BEST ANSWER

    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.

    import {
      Alert,
      Animated,
      Easing,
      FlatList,
      Linking,
      StyleSheet,
      Text,
      TouchableOpacity,
      View,
    } from "react-native";
    import React, {
      useEffect,
      useRef,
      useState,
      useMemo,
      useCallback,
    } 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
          onPress={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",
                    }),
                  },
                ],
              },
            ]}
          />
          {useMemo(
            () => (
              <FlatList
                data={DATA}
                extraData={selectedId}
                horizontal={true}
                keyExtractor={(item) => item.id}
                renderItem={renderItem}
                style={styles.flatList}
              ></FlatList>
            ),
            [selectedId]
          )}
        </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. 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:

    import {
      Alert,
      Animated,
      Easing,
      FlatList,
      Linking,
      StyleSheet,
      Text,
      TouchableOpacity,
      View,
    } from 'react-native';
    import React, {
      useEffect,
      useMemo,
      memo,
      useRef,
      useState,
      useCallback,
    } from 'react';
    
    import * as Location from 'expo-location';
    import * as geolib from 'geolib';
    
    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 = useCallback(
        ({ 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}
            />
          );
        },
        [selectedId]
      );
    
      const keyExtractor = useCallback((item) => item.id, []);
      return (
        <View style={styles.background}>
          <Text>{textPosition}</Text>
          <Animated.Text
            style={[
              styles.image,
              {
                transform: [
                  {
                    rotate: rotation.interpolate({
                      inputRange: [0, 360],
                      outputRange: ['0deg', '360deg'],
                      //extrapolate: "clamp",
                    }),
                  },
                ],
              },
            ]}>
            ^
          </Animated.Text>
          {useMemo(
            () => (
              <FlatList
                data={DATA}
                horizontal={true}
                keyExtractor={keyExtractor}
                renderItem={renderItem}
                style={styles.flatList}></FlatList>
            ),
            [keyExtractor, renderItem, DATA]
          )}
        </View>
      );
    }
    
    const styles = StyleSheet.create({
      background: {
        backgroundColor: 'white',
        flex: 1,
        // justifyContent: "flex-start",
        //alignItems: "center",
      },
      image: {
        flex: 1,
        // height: null,
        // width: null,
        //alignItems: "center",
      },
      flatList: {
        backgroundColor: 'white',
      },
    });
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search