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:
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:
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.
To complete the series, we’ll do the following:
The code is available in the project’s Github repository:
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.
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:
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 TailwindCSS, React, or Twilio.
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.
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.
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
… <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:
placeholder
text for the search inputopenOnFocus={true}
will open the content panel if the search field is focused.onStateChange={onSubmit}
is triggered whenever content in the autocomplete changesonSubmit={onSubmit}
is triggered when you hit the “enter” key, or a result is selectedonReset={onReset}
is triggered when you click on the x button in the inputThese 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.
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 :
HitComponent
which is the same component as the one we used in part 2 to render each hit in the sidebaronClick
handler, to map which function is called when users click on the HitComponent
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!
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:
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.
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 }: { item: IMapboxFeature }) { return ( & lt; HitComponent item = { 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…
And it seems to be working! Hooray!
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:
As we’ll be using Geolocation coordinates, this method will work for both plugins.
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);
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]])
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
. If currentStoreCoordinates
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:
viewport
, with updates every time the map is moved or zoomed in or outTwilio 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:
Let’s create our account and get the Account SID and Authentication token.
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.
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!
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.
If you need to buy a phone number, go to the left side bar and go to Phone Number > Manage > Buy a Phone Number
Next, select the country where you want to buy a phone number, and click buy.
After confirming, you’re now the proud owner of a new phone number!
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).
At the end, your messaging service should look similar to what’s shown in the following screenshot
Now, go to Properties and copy your messaging service SID.
With the messaging SID, navigate to Functions (you can search for it or find it under Explore Products > Developer Tools > Functions).
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.
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)
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!
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
Select Create a new application:
Select the type of application: Node. Proceed by clicking Next
Give your app a name, and select your data center.
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.
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:
CC_POST_BUILD_HOOK="yarn build" NODE_BUILD_TOOL="yarn"
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”.
To summarize what we’ve accomplished this week:
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:
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.
Clément Sauvage
Software Engineer, FreelancePowered by Algolia AI Recommendations
Clément Sauvage
Software Engineer, FreelanceJaden Baptista
Sarah Dayan
Principal Software EngineerFrançois Chalifour
Software Engineer