5

Building a Store Locator in React using Algolia, Mapbox, and Twilio – Part 3

 2 years ago
source link: https://www.algolia.com/blog/engineering/building-a-store-locator-in-react-using-algolia-mapbox-and-twilio-part-3/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Building a Store Locator in React using Algolia, Mapbox, and Twilio – Part 3

This article completes our 3-part series on building a Store Locator. Make sure to check out Building a Store Locator – Part 1 and Building a Store Locator – Part 2.

These days, ecommerce shoppers expect convenience and want the physical and online worlds to mesh, allowing them to conduct their business on whatever channel they want. This is what a Store Locator is built for, so that users can:

  • Search products online, then purchase in-store
  • Browse and compare products in person, then buy online
  • Shop and buy products online, then pick up the purchase in person
  • Return previously purchased items in person, rather than shipping them back

Despite that, we observe that too many websites don’t offer an elegant way for their users to find their “nearest store,” whether for testing, purchasing offline, or picking up a purchase made online.

Building a store locator may seem complex as it requires implementing a geo-search, address/POI (Point of Interest) search, and displaying results on a map. To make it as easy as possible for everyone to follow along, we’re going to create a Store Locator in React in three Live Coding Sessions accompanied by three blog posts, using:

  • Algolia, for its geo-search capabilities
  • Mapbox, for displaying a map and searching for locations and points of interest
  • Twilio, for sending texts once the order is ready
  • Clever Cloud, for hosting

What we’ve seen until now

In Part 1, we created all required accounts, scaffolded the React project, and indexed the dataset with our stores in an Algolia index before wrapping up by configuring its textual and business relevance.

In Part 2, we added Geo search capabilities into the app, using Algolia, React InstantSearch, and Mapbox, to display the results on a map connected to Algolia’s InstantSearch, thanks to the connectGeoSearch Higher-Order Component. We also added a “Search as I move the map” option, similar to what you can find on Airbnb or GetAround.

Today’s focus

To complete the series, we’ll do the following:

  • Add an Autocomplete dropdown menu using Algolia Autocomplete and the Mapbox Geocoding API, allowing users to find the nearest stores around a city, along with finding the stores themselves
  • Add SMS alerting to inform users if products are ready to collect, similar to what you can find on IKEA in their Buy online, pick up in-store (BOPIS) feature

The code is available in the project’s Github repository:

Adding Autocomplete

The Autocomplete experience

Autocomplete is a crucial part of the user journey on any website or app because of its effectiveness at guiding users. Your users don’t have to know the exact term they’re looking for—they can type a few letters, and the autocomplete menu shows available search options, ready to be selected.

It could be on an ecommerce platform, a search engine, or a map, where you don’t need to know the full address anymore.

What is Algolia Autocomplete?

​​Algolia Autocomplete is a free and open source JavaScript library. We recently refactored it completely to deliver the best developer experience, focusing on creating smooth search experiences with the autocomplete UX pattern.

By design, it provides an easy way to search in multiple sources, which can be Algolia indices or external APIs. Building federated search experiences has never been easier.

Today, we’ll create and add two sources to the Autocomplete plugin, so you can reuse them in your own code base:

  • The first source targets the Algolia index with the stores, so users can search for a store by its name or city
  • The second source targets the Mapbox Geocoding API, so users can search for any available city or point of interest all around the world

In our project, we use Autocomplete to search for locations and stores. As mentioned, it can be used in many other ways, for example, to power search experiences on documentation websites, via the Algolia DocSearch project, which you can find on websites such as TailwindCSSReact, or Twilio.

Installing AutocompleteJS

Let’s first install the required packages :

$ yarn add @algolia/autocomplete-js @algolia/autocomplete-theme-classic

Let’s also install @algolia/autocomplete-plugin-query-suggestions as we will use it in a moment.

Now, let’s create an <Autocomplete/> component.

Here, we won’t go into detail, but you can find all about this component in the docs.

The only thing you should notice here is the spread operator … props which will allow us to spread more props on component initialization.

Components/Autocomplete/Autocomplete.tsx

import React, { createElement, Fragment, useEffect, useRef } from 'react';
import { autocomplete, AutocompleteOptions } from '@algolia/autocomplete-js';
import { render } from 'react-dom';
type Optional<TObject, TKeys extends keyof TObject> = Partial<
  Pick<TObject, TKeys>
  > &
  Omit<TObject, TKeys>;
function Autocomplete = any>(
  props: Optional<
    AutocompleteOptions,
    'container' | 'renderer' | 'render'
  const containerRef = useRef(null);
  useEffect(() => {
      if (!containerRef.current) {
        return undefined;
    const search = autocomplete({
          container: containerRef.current!,
          renderer: { createElement, Fragment },
          render({ children }, root) {
            //@ts-ignore
            render(children, root);
          ...props,
    return () => {
          search.destroy();
      }, []);
  return <div className={'w-full'} ref={containerRef} />;
export default Autocomplete;

Now, let’s add it to our Header component as a child.

First, let’s update our header component to let it handle child components.

Components/Header/Header.tsx

const Header: React.FC = ({ children }) => {
return (
  <header className={“...''}>
    <Logo className={'w-auto h-16'} />
    <div className={'sm:w-full md:w-1/2'}>{children}</div>
  </header>

Then, let’s add the <Autocomplete/> component we’ve just created to this header in our App.tsx file

Components/App/App.tsx

<div className="App flex flex-col w-screen h-screen mx-auto bg-gray-50">
  <Header>
    {<Autocomplete
      initialState={{
        query: searchState.query,
      placeholder={'Enter address, zip code or store name'}
      openOnFocus={true}
      onStateChange={onSubmit}
      onSubmit={onSubmit}
      onReset={onReset}
      plugins={plugins}
    />}
  </Header>
  <div
    className={
      'sm:flex md:hidden w-full uppercase text-xl font-sans font-bold text-bold gap-4'

The Autocomplete component takes these props:

  • The placeholder text for the search input
  • The openOnFocus={true} will open the content panel if the search field is focused.
  • The onStateChange={onSubmit} is triggered whenever content in the autocomplete changes
  • The onSubmit={onSubmit} is triggered when you hit the “enter” key, or a result is selected
  • The onReset={onReset} is triggered when you click on the x button in the input

These methods are responsible for updating the state:

// Handle search results updates
const onSubmit = useCallback(({ state }) => {
  setSearchState((searchState) => ({
    ...searchState,
    query: state.query,
  }, []);
// Click on the little cross on autocomplete field
const onReset = useCallback(() => {
  setSearchState((searchState) => ({
    ...searchState,
    query: '',
}, []);

The useCallback hook returns a memoized version of the callback that only changes if one of the dependencies has changed. This can be useful to prevent unnecessary renders.

Adding the createQuerySuggestionsPlugin

Now that we’ve set the scene, it’s time to populate the Autocomplete component with data. You can use plugins to get data from many sources: such as your own database, third-party APIs, and Algolia. Algolia provides official plugins for some data sources, for example, an Algolia Index.

For our app, we’ll create two plugins :

  • createSuggestionsPlugin
  • mapboxGeocodingPlugin

Let’s create a new folder under src/ and call it AutocompletePlugins.

We’ll create a function called createSuggestionsPlugin which will be a wrapper around the createQuerySuggestionsPlugin provided by Algolia in the @algolia/autocomplete-plugin-query-suggestions package. This way, we can extend and enrich the behavior of the default plugin in order to search in our Algolia Store index.

const createSuggestionsPlugin = (
  searchClient: SearchClient,
  indexName: string,
  onSelectHandler: (query: string) => void,
  onClick: (item: any) => void,
  HitComponent: React.ComponentType
  ) => {
  return createQuerySuggestionsPlugin({
    searchClient,
    indexName,
    transformSource({ source }) {
      return {
        ...source,
        sourceId: 'AlgoliaStores',
        onSelect(params) {
          onSelectHandler(params.item.query);
        templates: {
          item({ item, components }: { item: any; components: any }) {
            return <HitComponent item={item} onClick={onClick} components={components} />;
export { createSuggestionsPlugin };

Our function takes :

  • An AlgoliaSearch client (we created this in blog post 2)
  • An index name (same)
  • HitComponent which is the same component as the one we used in part 2 to render each hit in the sidebar
  • An onClick handler, to map which function is called when users click on the HitComponent
  • An onSelectHandler, responsible for updating the state of the search input.

The search client and index name are called in the method initialization, and the others are used in the transformSource function of the plugin, which is responsible for transforming the data after it has been retrieved from our remote source—here, the Algolia API.

Now, let’s add this plugin to our Autocomplete instance, and check if everything is working as expected.

// Memoize plugins to then leverage useCallback hooks
const plugins = useMemo(() => {
  const querySuggestionPlugin = createSuggestionsPlugin(
    searchClient,
    indexName as string,
    (query) => {
      setSearchState((searchState) => ({
        ...searchState,
        query: query,
    (item) => console.log(item),
    SuggestionComponent
  return [querySuggestionPlugin];
}, []);

If we refresh our browser, we should see store names when we type in the search input, meaning that our plugin is fully functional!

Creating our own plugin to add POI Search with Mapbox

In addition to finding a specific store by searching by its name or city name, users should also be able to search for a given location and then find all stores around that location.

The best way to do that is a Geocoding service. Here, we’re going to use the Mapbox places API. Each new search from the end-user triggers a new request to this API, returning the name of the location and its latitude and longitude information. Once the user selects a result, we can use the lat/long attributes to trigger a GeoSearch against the Algolia API and retrieve stores around that location.

To create your custom plugin, you need to implement a given interface..

In our case, to reduce the number of API calls to the Mapbox API, we use a debouncing strategy which:

  • Avoids sending API calls at every single keystroke, but only when a time interval has passed, 300 ms in our case.
  • Is based on a setTimeout. At first, it sets the timeout at the value passed as parameter. If the function is called again before the timeout ends, then it clears it and sets it again with the same duration.

Let’s build the Mapbox API request

Before creating the plugin, we want to create the piece of code responsible for calling the Mapbox API.

const mapboxURL = `https://api.mapbox.com/geocoding/v5/mapbox.places/${encodeURIComponent(
    query
  )}.json`;
  const requestParams = {
    ...options,
    types: options.types,
    country: options.country,
    access_token: process.env.REACT_APP_MAPBOX_TOKEN,
  const endpoint = [
    mapboxURL,
    qs.stringify(requestParams, { arrayFormat: 'comma' }),
  ].join('?');

Note: You can find all available parameters for the request in the Mapbox docs.

Passing the results to Autocomplete as a Source Plugin

Now, we need to connect this function to our Autocomplete. The idea is to pass the results to the response awaited by Autocomplete, and wrap it in a function for easy reuse.

const createMapboxGeocodingPlugin = (
  options: IMapboxRequestParameters,
  HitComponent: React.ComponentType < { item: IMapboxFeature; onClick: (item: IMapboxFeature) => void;
  } > ,
  onClick: (result: IMapboxFeature) => void
) => {
  return {
    getSources({
      query
      query: string
      const mapboxURL = `https://api.mapbox.com/geocoding/v5/mapbox.places/${encodeURIComponent(
        query
      )}.json`;
      const requestParams = {
        ...options,
        types: options.types,
        country: options.country,
        access_token: process.env.REACT_APP_MAPBOX_TOKEN,
      const endpoint = [
        mapboxURL,
        qs.stringify(requestParams, {
          arrayFormat: 'comma'
      ].join('?');
      return debouncedFetch(endpoint)
        .then((response: any) => response.json())
        .then((data: IMapboxGeocodingResponse) => {
          return [{
            sourceId: 'mapboxPlugin',
            getItems() {
              return data.features;
            templates: {
              header() {
                return 'Mapbox Places results'
              item({
                item: IMapboxFeature
                return ( &
                  lt; HitComponent item = {
                  onClick = {
                    (item: IMapboxFeature) => onClick(item)
                  />
              noResults() {
                return 'No results.';

This code adds the new plugin to our Autocomplete instance.

Same as before, we now want to add the plugin to our Autocomplete instance.

const plugins = useMemo(() => {
  const mapboxGeocodingPlugin = createMapboxGeocodingPlugin({
      fuzzyMatch: true,
      autocomplete: true,
      types: ['country', 'place', 'poi'],
      country: ['FR', 'BE', 'GB', 'DE', 'CH', 'IT', 'ES'],
      access_token: process.env.REACT_APP_MAPBOX_TOKEN!,
    MapboxAddressComponent,
    (item) => {
      setCurrentStoreCoordinates([item.geometry.coordinates[0], item.geometry.coordinates[1]])
  const querySuggestionPlugin = createSuggestionsPlugin(
    searchClient,
    indexName as string,
    (query) => {
      setSearchState((searchState) => ({
        ...searchState,
        query: query,
    (item) => setCurrentStoreCoordinates([item._geoloc.lng, item._geoloc.lat]),
    SuggestionComponent
  return [mapboxGeocodingPlugin, querySuggestionPlugin];
}, []);

Note: In our implementation, we limit the results to a few countries and types of places, so the MapBox API returns the most relevant results. You can adjust these variables according to your needs.

types: ['country', 'place', 'poi'],
country: ['FR', 'BE', 'GB', 'DE', 'CH', 'IT', 'ES'],

Once that’s done, we can now check our app to see if Mapbox results have been added to our autocomplete. Let’s type “Marsel” in the search bar and see if we get shops located outside of the Marseille-Provence Airport…

Store Locator - Autocomplete preview

And it seems to be working! Hooray!

Passing clicked information to our InstantSearch instance

Now that users can select a result in the Autocomplete dropdown, we want to pass the information to the InstantSearch instance, so that we can:

  • Display the selected store location with its details
  • Run an Algolia Geo Search using the latitude and longitude of the selected location, to retrieve and display the closest stores from that location

As we’ll be using Geolocation coordinates, this method will work for both plugins.

Creating a place to store current coordinates

To store the selected place/shop in the Autocomplete, let’s create a state variable to store this data, we’ll call it currentStoreCoordinates

const [currentStoreCoordinates, setCurrentStoreCoordinates] = useState< [number, number] | null >(null);

Creating the handlers

Next, let’s create a method which updates this store according to the element we click. Items returned from createSuggestionsPlugin will have a _geoloc object with lat and lng keys, which themselves come from  the record stored in the Algolia index.

Items returned by createMapboxGeocodingPlugin will have a geometry object with a coordinates array.

First, let’s create a method to handle clicks on the suggestions:

const handleClickOnSuggestion = (hit: Hit) => {
  const { lat, lng } = hit._geoloc;
  const coordinates: [number, number] = [lng, lat];
  setCurrentStoreCoordinates(coordinates);
  setCurrentStoreName(hit.name)
  setIsOpen(true)

Add this to the querySuggestionPlugin instance as the onClick handler.

Now, let’s update the onClick handler for the Mapbox plugin:

(item) => setCurrentStoreCoordinates([item.geometry.coordinates[0], item.geometry.coordinates[1]])

Passing latitude and longitude to InstantSearch and to our components

In Part 2, we configured our <InstantSearch/> wrapper:

<Configure aroundLatLngViaIP={true} />

Now, we’ll leverage the aroundLatLng property to trigger a Geo Search based on the latitude and longitude parameters.

&lft;Configure
    aroundLatLngViaIP={!currentStoreCoordinates?.length}
    aroundLatLng={!!currentStoreCoordinates?.length ? '' : currentStoreCoordinates?.join(',')} />

This time, we’re setting aroundLatLngViaIP to either true or false, depending on the state of currentStoreCoordinates. If  currentStoreCoordinates is empty, we set aroundLatLngViaIP to true and perform a Geo Search based on the user’s IP address, otherwise we pass the coordinates as strings to aroundLatLng to perform a GeoSearch with these coordinates! That’s all!

Now, let’s add currentStore={currentStoreCoordinates} to our StoreComponent and to the Map to highlight the selected store.

With this we’ve completed the Autocomplete part! We now have a fully functional Store Locator, which allow users to find stores:

  • Near their current location, thanks to their IP address and from the page load
  • Based on the current viewport, with updates every time the map is moved or zoomed in or out
  • Using keyword search via the autocomplete, searching either for a store by its name or the city’s name
  • Using keyword search via the autocomplete, searching for a location name and find all stores nearby

Adding the Twilio SMS experience

What is Twilio?

Twilio is the leading CPaaS (Communication Platform as a Service) company created in 2008 by Jeff Lawson in San Francisco. Companies like Deliveroo, Uber, Amazon GetAround are customers, so it’s likely that you use Twilio’s services without knowing.

They serve many communications channels, from SMS, voice calls, and push notifications, all the way up to fully configurable call centers for enterprises. They also expanded into emails and customer analytics via the acquisition of SendGrid and Segment.

In this project, we’ll use SMS for three reasons:

  • BOPIS feature (buy online, pick-up in-store) to receive the store location by text
  • Out-of-Stock notification, so you get a text whenever the product is back in-stock
  • Marketing notifications

Creating your account

Let’s create our account and get the Account SID and  Authentication token.

Twilio.com homepage

Go to Twilio.com and click “Sign up”. Enter the necessary information to create your free Twilio account.

Once you’re logged in, you’ll land on the “console”, where you find two critical pieces of information: your Twilio Account SID and Auth Token.

Twilio Dashboard - Account info

We’ll use their serverless feature (Functions) and not our own back end. If one day you need to send API requests from your servers to Twilio, you know where to find it!

Getting your first phone number and creating your first SMS request

To send SMS (especially in the US and Canada) you’ll need to buy a dedicated phone number, for other countries, you can have a look at Twilio’s regulatory guidelines available on Twilio Website ]

If you want to send an SMS in France, you can skip this step, as buying a French phone number is tricky. This isn’t Twilio’s fault, but a consequence of the rules of the ARCEP (Autorité de Régulation des Communications Électroniques et des Postes). You can go to “Creating your first messaging service”. For France, we’ll use Alpha Senders., which identifies your brand as the sender—it’s required by law in France.

Twilio SMS preview

If you need to buy a phone number, go to the left side bar and go to Phone Number > Manage > Buy a Phone Number

Twilio Dashboard - Sidebar

Next, select the country where you want to buy a phone number, and click buy.

Twilio Dashboard - Buy Number

After confirming, you’re now the proud owner of a new phone number!

Creating your first messaging service

After setting up a new phone number (or an Alpha Sender if you’re in France), type “Messaging Service” in the search bar type “Messaging Service” (You can appreciate another autocomplete experience).

Twilio Dashboard - Dropdown selection
  • From the list of suggestions, select Create Messaging Service
  • Name your new messaging service, for example, Store Locator and select Notify my users.
  • Next, click Add Senders and then Alpha Sender and enter your business name.
  • If you bought a phone number, you can select your phone number from a list.

At the end, your messaging service should look similar to what’s shown in the following screenshot

Twilio Dashboard - sender pool

Now, go to Properties and copy your messaging service SID.

Twilio Dashboard - Messaging service settings

Creating a function to send SMS

With the messaging SID, navigate to Functions (you can search for it or find it under Explore Products > Developer Tools > Functions).

Twilio Dashboard - explore products

Once again, create a service, give it a name, and click Next. An IDE opens in the browser.

Create a new function by clicking on + Add > new function and name it “place-order”. Set its visibility to “public” as we’ll need to call it from outside of Twilio.

Now, go to Dependencies and add the date-fns package to handle dates.

Twilio Misc - dependency label

In the module field, type date-fns and click add.

Now, let’s add the code responsible for sending the messages:

// Let's require date-fns functions, we'll use them to manipulate dates.
const parse = require('date-fns/parse');
const format = require('date-fns/format');
exports.handler = async function(context, event, callback) {
  // Let's fix CORS, this is required because we are calling this function from outside the twil.io domain
  const response = new Twilio.Response();
  response.appendHeader('Access-Control-Allow-Origin', 'https://localhost:3000'); // Change this by you domain
  response.appendHeader('Access-Control-Allow-Methods', 'GET, OPTIONS, PUT, POST, DELETE');
  response.appendHeader('Access-Control-Allow-Headers', 'Content-Type');
  // Now we're grabbing data from the request payload
  const {
    store,
    customer,
    time,
    phoneNumber
  } = event;
  // We're using format to display a nice date in the SMS
  const formattedDate = format(parse(time, 'dd-MM-yyyy::HH:mm', new Date()), "eee, d LLLL yyyy 'at' kk:mm");
  // Let's write the message
  const message = `Hey ${customer}. Your order will be ready to pick-up at ${store} on ${formattedDate}. Thanks for beeing a Spencer & William Customer`;
  // Time to hit the send button !
  const client = context.getTwilioClient();
  await client.messages.create({
    body: message,
    messagingServiceSid: 'MG9378a33499ec5b0201374e6e8b0adb67',
    to: phoneNumber
  // Don't forget the callback otherwise you'd raise an error as the function will keep running
  return callback(null, response)

And that’s all! Click Save and Deploy all, it will take ~2 minutes, and a green check mark will appear next to your function name. The function is now available from the outside by calling the Twilio API with our Twilio credentials.

Note: you can test it using an API explorer such as Postman or Paw.cloud (on macOS)

Twilio Dashboard - Deploy function

Calling the place-order function from your code

You’ll find the form component in the Components/ directory. It appears on-screen when you select a store from the list or on the map.

It has three fields: for customer name, pick-up date, and phone number. Now we need to perform the request when we click the “Place Order” button.

We’ll use axios to perform the request, but you can use your preferred library.

yarn add axios

Let’s populate the sendRequest() method:

async sendRequest() {
  const {
    customer,
    time,
    phoneNumber,
    phoneCountry
  } = this.state
  const {
    store
  } = this.props
  this.setState({
    isLoading: true
  await axios.post(process.env.REACT_APP_TWILIO_FUNCTION_URL as string, {
    store,
    customer,
    time,
    phoneNumber: parsePhoneNumber(phoneNumber, phoneCountry).number
  this.setState({
    isLoading: false

And that’s all, your app will now send text messages to your customer. These were our last lines of code for this app, it’s now time to deploy it to Clever Cloud!

Deploying your app on Clever Cloud

As a final step, we’re going to deploy the Store Locator to Clever Cloud hosting platform, so you can show your freshly built app to your friends and colleagues.

Go to your Clever Cloud dashboard and click on + Create / An application 

CleverCloud Dashboard - overview

Select Create a new application:

CleverCloud Dashboard - create new app

Select the type of application: Node. Proceed by clicking Next

CleverCloud - add node dialog

Give your app a name, and select your data center.

CleverCloud Dashboard - Pick server location

Finally, select Configuration Provider. This will replace our .local.env file and inject variables in our app at build time. Then click Next and give your configuration provider a name.

CleverCloud Dashboard - Add Add-on

On the next screen, click on Expert and copy / paste your .local.env file.

Add these two lines to the environment variables to use yarn and to build the project after the dependencies are installed:

CleverCloud Dashboard - Env variable setup
CC_POST_BUILD_HOOK="yarn build"
NODE_BUILD_TOOL="yarn"

CleverCloud Dashboard - Env variable setup edit

Click on the green button twice and click on your node machine in the left menu. Follow the steps on top of your screen to add Clever’s git URL to your project and push it! It’ll be available in 3 to 4 minutes using the URL under “domain names”.

CleverCloud Dashboard - App infos

Wrapping up

To summarize what we’ve accomplished this week:

  • We’ve added an Autocomplete dropdown that complements the existing app experience, allowing users to search for stores by their name, or by location name, so they can then find the nearest store nearby that location.
  • We’ve added Twilio support to send texts to our customers.
  • We’ve deployed our app on Clever cloud. 🎊

And that’s a wrap for our series about building a Store Locator in React with Algolia, Mapbox, and Twilio. But there are (as always) many more features you can add:

  • Leverage the store locator to offer a BOPIS feature
  • Send emails rather than SMS, with Twilio SendGrid
  • Track and update most clicked store with Twilio Segment
  • Add payment options with Stripe APIs

I hope you liked this series! And to paraphrase a CEO and developer I really admire: I can’t wait to see what you’ll build.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK