GeoSearch with React Native

Hi,

I’ve implemented GeoSearch using Algolia and React Native however I’m unsure if I have done it in the correct manner. I have read some examples that show how to use the map with a bounding box and display results only within the boundaries.

With my present implementation I am unable to get the map boundaries and pass it to my connectedGeoSearch in order to retrieve the hits.

I have a couple of questions:

  • What do you suggest as the best structure for implementing GeoSearch with React Native?
  • Is it possible to use aroundRadius for all results, not just those shown in the map?

My Code is as follows:

SearchModal (Container for GeoSearch):

import React from 'react'
import {
  View,
  StyleSheet,
  TouchableOpacity
} from 'react-native'

import { InstantSearch, connectConfigure } from 'react-instantsearch-native'
import algoliasearch from 'algoliasearch/lite'
import Geolocation from '@react-native-community/geolocation'

import ButtonLarge from '../components/Buttons/ButtonLarge'
import Icon from 'react-native-vector-icons/MaterialIcons'
import MapSearch from '../components/MapSearch'

const searchClient = algoliasearch('FOM3R1S7Y3',
  ''
)

const Configure = connectConfigure(() => null);

export default class SearchModal extends React.Component {

  closeModal = () => {
    this.props.navigation.goBack();
  }

  render() {
    const {navigation} = this.props;
    const latitude = navigation.getParam("latitude", "No Data")
    const longitude = navigation.getParam("longitude", "No Data")
    return (
      <InstantSearch
      searchClient={searchClient}
      indexName="Places"
      >
      <View style={styles.buttonsTop}>
      <ButtonLarge title={"Filters"} />
      </View>
      <TouchableOpacity onPress={this.closeModal} style={styles.button}>
        <Icon name={"close"} color={"#595959"} size={24} />
      </TouchableOpacity>
      <MapSearch
        initialPosition={{
          lat: latitude,
          lng: longitude,
        }}
        currentRefinement={{
          latitude, longitude
        }}
        latitude={latitude}
        longitude={longitude}
        enableRefine={true}
        isRefinedWithMap={true}
      />
    </InstantSearch>
    )
  }
}

const styles = StyleSheet.create({
  buttonsTop: {
    position: 'absolute',
    top: '5%',
    left: '1%',
    zIndex: 100,
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center'
  },
  button: {
    position: 'absolute',
    top: '5%',
    right: '1%',
    zIndex: 100,
    backgroundColor: '#FFFFFF',
    borderRadius: 30,
    justifyContent: 'center',
    alignItems: 'center',
    shadowColor: '#C9C9C9',
    shadowOffset: {width: 0, height: 3},
    shadowOpacity: 0.6,
    shadowRadius: 20,
    height: 30,
    width: 30,
    marginRight: 15
  },
})

    MapView (connected with GeoSearch)

        import React, { PureComponent } from 'react'
        import { Dimensions, StyleSheet, View, Animated } from 'react-native';
        import { connectGeoSearch, connectConfigure } from 'react-instantsearch-native'
        import MapView, {Marker} from 'react-native-maps'

        import {mapStyle} from '../utils/mapstyle'
        import CustomMarker from './CustomMarker'
        import SearchButton from './SearchButton'
        import ItemResults from './ItemResults';
        import MapSort from './MapSort'
        import Loader from '../components/Loader'

        const { height, width } = Dimensions.get('screen');
        const ASPECT_RATIO = width / height
        const LATITUDE_DELTA = 0.03
        const LONGITUDE_DELTA = LATITUDE_DELTA * ASPECT_RATIO
        const CARD_WIDTH = width / 2;
        const CARD_HEIGHT = CARD_WIDTH * .65;
        const CONTAINER_HEIGHT = CARD_HEIGHT + (height * .14)

        const Results = React.forwardRef((props, ref) => (
          <ItemResults forwardedRef={ref} {...props} />
        ));

        class MapSearch extends PureComponent {
          constructor(props){
            super(props);

            this.state = {
              showButton: false,
              current: 0,
              tracksViewChanges: false,
              loaded: false,
              region: {
                latitude: null,
                longitude: null,
                latitudeDelta: LATITUDE_DELTA,
                longitudeDelta: LONGITUDE_DELTA
              }
            }
            this.animation = new Animated.Value(0);
            this.ItemResults = React.createRef();
          }

          componentDidUpdate = (prevProps) => {
            if(this.props.hits !== prevProps.hits && this.props.hits.length) {
              this.toFirstMarker();
            }
          }

          componentDidMount = () => {
            this.loadMarkers();
             this.animateMarkers();
             this.selectedMarker();
           }

          mapLoaded = () => {
            this.setState({
              loaded: true
            })
            this.toFirstMarker();
          }

          toFirstMarker  = () => {
            const { current } = this.state;
            const position = this.props.hits[current]

            setTimeout(() => this.map.animateToRegion(
              {
                latitude: position._geoloc.lat,
                longitude: position._geoloc.lng,
                longitudeDelta: LONGITUDE_DELTA,
                latitudeDelta: LATITUDE_DELTA
              },
              200
            ), 500)
          }


          animateMarkers = () => {
            this.animation.addListener(({value}) => {
              let index = Math.floor(value / CARD_WIDTH + 0.3);
              if (index >= this.props.hits.length) {
                index = this.props.hits.length -1;
              }
              if (index <= 0) {
                index = 0
              }
              clearTimeout(this.regionTimeout);
              this.regionTimeout = setTimeout(() => {
                if (this.index !== index) {
                  this.index = index;
                  const position = this.props.hits[index];
                  this.map.animateToRegion(
                    {
                      latitude: position._geoloc.lat,
                      longitude: position._geoloc.lng,
                      longitudeDelta: LONGITUDE_DELTA,
                      latitudeDelta: LATITUDE_DELTA
                    },
                    300
                  );
                }
              }, 10);
            });
          }

          selectedMarker = () => {
            this.animation.addListener(({value}) => {
              let position = value - 10;
              let index = Math.floor(value / CARD_WIDTH);
              if (index >= this.props.hits.length) {
                index = this.props.hits.length -1;
              }
              if (index <= 0) {
                index = 0
              }
              this.setState({
                current: index
              });
            })
          }

          showSearchButton = () => {
            this.setState({showButton: true})
          }

          redoSearch = () => this.map.getMapBoundaries().then((result) => {
            const ne = result.northEast;
            const sw = result.southWest;
            this.props.refine({
              northEast: {
                lat: ne.lat,
                lng: ne.lng
              },
              southWest: {
                lat: sw.lat,
                lng: sw.lng
              }
            })
          }).then(() => {
            this.setState({
              showButton: false
            })
          })
          .catch(error => {
            console.log(error.message)
          })

          scrollToIndex = (value) => {
            this.ItemResults.current.snapToItem(value, animation = false, fireCallBack = false);
            this.setState({
              current: value
            })
          }

          render() {
          const { hits, refine, latitude, longitude } = this.props;
          const { showButton, current } = this.state;
            return ( <View>
              {showButton &&
                <SearchButton title={"Redo search in this area"} onPress={this.redoSearch} />
              }
                <MapView
                ref={map => this.map = map}
                provider={MapView.PROVIDER_GOOGLE}
                onMapReady={() => this.mapLoaded}
                initialRegion={{
                  latitude: latitude,
                  longitude: longitude,
                  latitudeDelta: LATITUDE_DELTA,
                  longitudeDelta: LONGITUDE_DELTA
                }}
                showsUserLocation={true}
                style={styles.map}
                customMapStyle={mapStyle}
                onPanDrag={this.showSearchButton}
                >
                {hits.map((hit, i) => {
                  return (
                <Marker
                key={hit.id}
                coordinate={{
                  latitude: hit._geoloc.lat,
                  longitude: hit._geoloc.lng,
                  latitudeDelta: 0,
                  longitudeDelta: 0
                }}
                zIndex={current === i ? 2 : 1}
                onPress={() => this.scrollToIndex(i)}
                tracksViewChanges={current === i ? true : false}
                >
                <CustomMarker name={hit.rating} current={current === i} />
                </Marker>
                )})}
                </MapView>
              <View style={styles.resultsContainer}>
              <MapSort defaultRefinement="Places"
                items={[
                  { value: 'Places', label: 'Places' },
                  { value: 'Items', label: 'Items' },

                ]} />
              <View style={styles.results}>
                <Loader loadingStyle={styles.loader} />
                <ItemResults forwardedRef={this.ItemResults} position={"start"} slideScale={1} data={hits} small={false} fontSize={14} animation={this.animation} />
              </View>
            </View>
          </View>
            );
          }
        }

        export default MapSearch = connectGeoSearch(MapSearch);

        const styles = StyleSheet.create({
          map: {
            position: 'relative',
            width: '100%',
            height: '100%',
          },
          resultsContainer: {
            position: 'absolute',
            bottom: '0%',
            flexDirection: 'column',
            alignItems: 'center',
            alignSelf: 'center',
            width: '100%',
          },
          results: {
            backgroundColor: '#FFFFFF',
            borderTopLeftRadius: 8,
            borderTopRightRadius: 8,
            width: "100%",
            height: CONTAINER_HEIGHT,
            padding: 12,

          },
          loader: {
            height: CONTAINER_HEIGHT,
            width: "100%",
            justifyContent: "center",
            alignItems: "center",
            zIndex: 10000
          }
        })

Please let me know if you can help, any guidance is appreciated :slight_smile:

1 Like

Hi @m.nikolic256 . When you say “What do you suggest as the best structure for implementing GeoSearch with React Native?” is there anything in particular you are looking for?

While we don’t have a specific example of GeoSearch for React Native, you can find a GeoSearch example in our documentation. The concepts remain the same, only the rendering will change. We also have an example of a complete application built with React Native.

Where in your current implementation are you running into issues getting the map boundaries? I see you’re trying to get them using a navigation object passed as props.

In terms of using aroundRadius – this can’t be used concurrently as insideBoundingBox. aroundRadius is dependent on setting a central spot (and then setting the radius away from that lat/long). insideBoundingBox works by scanning a box (or polygon) for results.

I guess my confusion is with which component needs to be wrapped with the geoSearch connector, is it the mapview itself or a child of map view? Or is it something completely different like a parent container for both?

In my current implementation i can get mapBoundaries via the MapView component, looking at your example, the customGeoSearch component receives defaultRefinement props with the boundaries passed to it. In my present implementation it is not possible as my mapView component is wrapped with the geoSearch connector.

What I am trying to do is open a modal that will take the users present location and show results within the map boundaries of their present location. The user will be able to drag the map and redo their search with the adjusted boundaries. Struggling a little to see how to best implement this.

Please let me know if you need any more information.

I have it working currently, have figured out a mistake I made. However one issue i still have currently is that all of my data is loaded initially on the map and it is not taking into account any bounding box settings. If I refine the results after panning on the map, results are refreshed but i’d like it to load the correct data on the initial render.

Are there any examples for working with a bounding box or the aroundLatLng setting?

Your help is appreciated :slight_smile:

Nevermind, i got it :smiley: