Nerd For Tech
Published in

Nerd For Tech

(React Native) Custom Search Bar with Google Places Autocomplete API

In many apps, you will see some suggestions listed right under the search bar as you type. To implement this feature in React Native, there is a library called “react-native-google-places-autocomplete”. However, it is not that difficult to implement a simple version of the same feature. If you can build your own search bar, you will have more flexibility in terms of styling and functionality. In this article, I will show you how to build your own search bar with autocomplete functionality.

My Environment

  • ReactNative : 0.63.4
  • Typescript: 3.8.3
  • macOS Big Sur: 11.1

Notes about potential compile issues on iOS and Android

  • iOS:
    If you try to run the app on a simulator using npx react-native run-ios, it might not work. There seems to be a problem with the Flipper library used for the React Native version at the time of this article (v 0.63.4). If you encountered a compile error, you can try running the app from Xcode, or you can try temporary solutions from this GitHub issue.
    React Native 0.64 has been released while I am writing this article, so hopefully, the issue is resolved in that patch.
  • Android:
    If you recently upgraded your OS to macOS Big Sur, npx react-native run-android might not work well because of problems with JAVA_HOME path configuration. In order to make it work, I uninstalled the old version of Java (version 9) and installed jEnv and some Java versions from AdoptOpenJDK via Homebrew.
    Setting up a Java environment on your macOS is off the topic, so I won’t cover it in this article. However, I left some references to articles on this matter at the end of this article so you can refer to them.

1. Generate and Set up the project

In order to generate the project, you can use the Typescript template from React Native Community.

npx react-native init YourProjectName --template react-native-template-typescript

Note: The above command won’t work if you have an old version of react-native or react-native-cli installed globally, and you need to uninstall it to make it work. See the official documentation from React Native Community at https://reactnative.dev/docs/typescript and https://github.com/react-native-community/cli for more information.

When I generated the project, it came with the older version of eslint and jest for some reasons. So, I updated them following the steps below.

// Uninstall @react-native-community/eslint-config once to work with the latest configuration, and re-install
npm uninstall @react-native-community/eslint-config && npm i -D @react-native-community/eslint-config
// Uninstall eslint and re-install
npm uninstall eslint && npm i -D eslint
// Uninstall jest related dependencies and re-install
npm uninstall jest @types/jest babel-jest && npm i -D jest @types/jest babel-jest

Those commands should update the dependencies to the latest configured version. You can optionally add the typescript-eslint plugin and prettier with the following commands.

// Add the typescript plugin for eslint
npm i -D @typescript-eslint/parser @typescript-eslint/eslint-plugin
// Add prettier
npm i -D prettier eslint-config-prettier eslint-plugin-prettier

This should be enough for the basic project setup.

2. Build the basic search bar view

Next, we need to create a search bar component. Create a folder named “components” and put your component file under the directory. Then, add the following code. This is just the code to display a search bar with basic styling, so I won’t explain it in detail.

// ./components/SearchBarWithAutocomplete.tsximport React, { FunctionComponent } from 'react'
import {
StyleSheet,
View,
TextInput,
ViewStyle
} from 'react-native'
type SearchBarProps = {
value: string
style?: ViewStyle | ViewStyle[]
onChangeText: (text: string) => void
}
const SearchBarWithAutocomplete: FunctionComponent<SearchBarProps> = props => {
const {
value,
style,
onChangeText
} = props
const {
container,
inputStyle
} = styles
const passedStyles = Array.isArray(style) ? Object.assign({}, ...style) : style
return (
<View style={[container, { ...passedStyles }]}>
<TextInput
style={inputStyle}
placeholder='Search by address'
placeholderTextColor='gray'
value={value}
onChangeText={onChangeText}
returnKeyType='search'
/>
</View>
)
}
const styles = StyleSheet.create({
container: {
justifyContent: 'center'
},
inputStyle: {
paddingVertical: 16,
paddingHorizontal: 16,
backgroundColor: '#cfcfcf',
borderRadius: 20,
color: 'black',
fontSize: 16
}
})
export default SearchBarWithAutocomplete

We also need to modify the App.tsx file as below to display the search bar that we just created.

// App.tsximport React, { useState } from 'react'
import {
SafeAreaView,
StyleSheet,
StatusBar,
View
} from 'react-native'
import SearchBarWithAutocomplete from './components/SearchBarWithAutocomplete'const App = () => {
const [search, setSearch] = useState({ term: '' })
const { container, body } = stylesreturn (
<>
<StatusBar barStyle="dark-content" />
<SafeAreaView style={container}>
<View style={body}>
<SearchBarWithAutocomplete
value={search.term}
onChangeText={(text) => setSearch({ term: text })}
/>
</View>
</SafeAreaView>
</>
)
}
const styles = StyleSheet.create({
container: {
flex: 1
},
body: {
paddingHorizontal: 20
}
})
export default App

If you build and run your app, you should see the search bar displayed on your screen.

3. Get Google Places API key

In order to implement autocomplete functionality, we need to get an API for Google Places API.

3.1. Go to Google Cloud Platform

Visit Google Cloud Platform console to get an API key to use “Google Maps Javascript API”.

3.2. Create a project and enable the API

First, create a new project on the console and navigate to “APIs and Services” on the left drawer navigation. Then, click on “ENABLE APIs AND SERVICES” on the dashboard.

APIs and Services dashboard

On the next screen, search for “Places API” and enable it.

3.3. Generate an API key

After enabling the API, you need to create a key to actually use the API. navigate to “Credentials” under “APIs & Services”, and click on “CREATE CREDENTIALS” in the upper middle of the screen and choose “API key”.

Choose “Credentials” under “APIs & Services”
Choose “API key”

This will generate an API key, so keep it somewhere safe because you need it later to use the API in the app.

Take a note of the key

3.4 Create a billing account and link it to the project

In order to get results from the API, your Google Cloud project need to have a billing account linked to your project. You can create a billing account and link it to your project on the “Billing” page.

Create a billing account and link it to your project

Note: In order to avoid unwanted billings, I recommend that you disable the API or delete the project on the Google Cloud console at the end of this tutorial.

4. Implement the functionality to send requests to the API

Now, we need to modify the app to be able to send requests to the Places API using the API key. First, install axios , the library used to send HTTP requests, with the following command.

npm i axios

Next, modify the App.tsx file as below.

// App.tsx
// Duplicates are omitted and replaced with "..."
import React, { useState } from 'react'
import {
...
} from 'react-native'
// ==== Change No.1 ====
import axios from 'axios'
import SearchBarWithAutocomplete from './components/SearchBarWithAutocomplete'// ==== Change No.2 ====
const GOOGLE_PACES_API_BASE_URL = 'https://maps.googleapis.com/maps/api/place'
// ==== Change No.3 ====
/**
* Prediction's type returned from Google Places Autocomplete API
*
https://developers.google.com/places/web-service/autocomplete#place_autocomplete_results
*/
export type PredictionType = {
description: string
place_id: string
reference: string
matched_substrings: any[]
tructured_formatting: Object
terms: Object[]
types: string[]
}
const App = () => {
// === Change No.4 ====
const [search, setSearch] = useState({ term: '', fetchPredictions: false })
// ==== Change No.5 ====
const [predictions, setPredictions] = useState<PredictionType[]>([])
const { container, body } = styles // ==== Change No.6 ====
/**
* Grab predictions on entering text
* by sending reqyest to Google Places API.
* API details:
https://developers.google.com/maps/documentation/places/web-service/autocomplete
*/

const onChangeText = async () => {
if (search.term.trim() === '') return
if (!search.fetchPredictions) return
const apiUrl = `${GOOGLE_PACES_API_BASE_URL}/autocomplete/json?key=${env.GOOGLE_API_KEY}&input=${search.term}`
try {
const result = await axios.request({
method: 'post',
url: apiUrl
})
if (result) {
const { data: { predictions } } = result
setPredictions(predictions)
}
} catch (e) {
console.log(e)
}
}
// ==== Change No. 7====
/**
* Grab lattitude and longitude on prediction tapped
* by sending another reqyest using the place id.
* You can check what kind of information you can get at:
*
https://developers.google.com/maps/documentation/places/web-service/details#PlaceDetailsRequests
*/
const onPredictionTapped = async (placeId: string, description: string) => {
const apiUrl = `${GOOGLE_PACES_API_BASE_URL}/details/json?key=${env.GOOGLE_API_KEY}&place_id=${placeId}`
try {
const result = await axios.request({
method: 'post',
url: apiUrl
})
if (result) {
const { data: { result: { geometry: { location } } } } = result
const { lat, lng } = location
setShowPredictions(false)
setSearch({ term: description })
}
} catch (e) {
console.log(e)
}
}
return (
<>
<StatusBar barStyle="dark-content" />
<SafeAreaView style={container}>
<View style={body}>
<SearchBarWithAutocomplete
value={search.term}
// ==== Change No. 8 ====
onChangeText={(text) => {
setSearch({ term: text, fetchPredictions: true })
onChangeText()
}}

showPredictions={showPredictions}
predictions={predictions}
onPredictionTapped={onPredictionTapped}

/>
</View>
</SafeAreaView>
</>
)
}
const styles = StyleSheet.create({
container: {
flex: 1
},
body: {
paddingHorizontal: 20
}
})
export default App

Here is the explanation for the changes.

  1. Import axios
  2. Store base URL in a constant so it can be re-used
  3. Define PredictionType following the official API docs from Google
  4. Add fetchPredictions to the search state
    fetchPredictions is used to decide if the app should send a request to the API.
  5. Add a new state
    predictions : Predictions to display
  6. onChangeText method
    The method to be called whenever the text is changed. It grabs prediction data to display by sending a request to the API. If the input is empty, it returns right away so it doesn’t send an unnecessary request.
    It also returns right away if fetchPredictions is set to be false . This line is needed to prevent the app from sending a request when we update the text in the search bar on prediction tapped.
  7. onPredictionTapped method
    The method to be called when a prediction is tapped. It sends another request based on the prediction information to grab more detailed information such as latitude and longitude. Latitude and longitude are not used in this app, but this feature comes in handy if you want to use the search bar with a map. It also sets the entire address into the search bar without showing predictions.
  8. Pass methods and predictions down to the search bar components

Also, add new props in the search bar components’ type definition to resolve linting errors. The type definition should look like this now.

// ./components/SearchBarWithAutocomplete.tsx// ==== Change No. 1 ====
import { PredictionType } from '../App'
type SearchBarProps = {
value: string
style?: ViewStyle | ViewStyle[]
onChangeText: (text: string) => void
// ==== Change No. 2====
predictions: PredictionType[]
showPredictions: boolean
onPredictionTapped: (placeId: string, description: string) => void

}
  1. Import PredictionType from App.tsx
    In order to organize types and keep your project clean, you can also have a file that stores type definitions to import them into other files. For example, you can create a file named types.ts and store type definitions there.
  2. Add new prop types

5. Display predictions

At this point, the app should be able to get the necessary data to display. Next, we will modify the search bar component to display predictions.

// SearchBarWithAutocomplete.tsx
// Duplicates are omitted and replaced with "..."
// ==== Change No.1 ====
import React, { FunctionComponent, useState } from 'react'
// ==== Change No.2 ====
import {
...
FlatList,
TouchableOpacity,
Text

} from 'react-native'
...const SearchBarWithAutocomplete: FunctionComponent<SearchBarProps> = props => {
// ==== Change No.3 ====
const [inputSize, setInputSize] = useState({ width: 0, height: 0 })
const {
value,
style,
onChangeText,
// ==== Change No.4 ====
onPredictionTapped,
predictions,
showPredictions

} = props
const {
container,
inputStyle
} = styles
const passedStyles = Array.isArray(style) ? Object.assign({}, ...style) : style
// ==== Change No.5 ====
const inputBottomRadius = showPredictions ?
{
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0
}
:
{
borderBottomLeftRadius: 20,
borderBottomRightRadius: 20
}
// ==== Change No.6 ====
const _renderPredictions = (predictions: PredictionType[]) => {
const {
predictionsContainer,
predictionRow
} = styles
const calculatedStyle = {
width: inputSize.width
}

return (
<FlatList
data={predictions}
renderItem={({ item, index }) => {
return (
<TouchableOpacity
style={predictionRow}
onPress={() => onPredictionTapped(item.place_id, item.description)}
>
<Text
numberOfLines={1}
>
{item.description}
</Text>
</TouchableOpacity>
)
}}
keyExtractor={(item) => item.place_id}
keyboardShouldPersistTaps='handled'
style={[predictionsContainer, calculatedStyle]}
/>
)
}
return (
<View style={[container, { ...passedStyles }]}>
<TextInput
// ==== Change No.7 ====
style={[inputStyle, inputBottomRadius]}
placeholder='Search by address'
placeholderTextColor='gray'
value={value}
onChangeText={onChangeText}
returnKeyType='search'
// ==== Change No.8 ====
onLayout={(event) => {
const { height, width } = event.nativeEvent.layout
setInputSize({ height, width })
}}

/>
// ==== Change No.9 ====
{showPredictions && _renderPredictions(predictions)}

</View>
)
}
const styles = StyleSheet.create({
...
// ==== Change No.10 ====
predictionsContainer: {
backgroundColor: '#cfcfcf',
padding: 10,
borderBottomLeftRadius: 10,
borderBottomRightRadius: 10
},
predictionRow: {
paddingBottom: 15,
marginBottom: 15,
borderBottomColor: 'black',
borderBottomWidth: 1,
}

})
export default SearchBarWithAutocomplete

Here is the summary of the changes.

  1. Import useState from react
  2. Import FlatList, TouchableOpacity and Text from react-native
  3. Add the new state inputSize
    inputSize
    is used to match the prediction box width and the TextInput width. When TextInput is rendered, its width and height will be stored in this state via onLayout prop.
  4. Extract new props
  5. Adjust the input style
    Adjust the input style when predictions are being displayed. Otherwise, there will be unwanted spaces between the TextInput and the predictions because of borderBottomRadius.
  6. _renderPredictions method
    The _renderPredictions method is used to render predictions right below TextInput.
  7. Apply the input style change
  8. Set inputSize via onLayout
  9. Render predictions when showPredictions is true
  10. Styling for the predictions

Now, you should see predictions as you type something in the search bar.

6. A critical issue with our current implementation

Now, the app is able to get predictions from the API and display them in the app. However, there is an issue. With our current implementation, the app sends a request every time the user enters something. This can be very bad in terms of performance. In order to fix this issue, we can make use of the debounce function.

7. What is a debounce function?

A debounce function is used to delay a function call. For instance, when it is used for a search bar, it waits for the user to stop typing so it doesn’t make unnecessary function calls.

You might be wondering why we can’t just use the built-in JavaScript function setTimeout . The problem with setTimeout is that the callback passed to it still gets called every time it detects changes in the dependencies. It can delay a function call, but the number of executions is the same without it.

8. Create a custom hook “useDebounce”

Now, in order to have the debounce functionality in our app, we will implement a custom useDebounce hook. Create a directory called hooks and put a file called useDebounce.ts under the directory with the following code.

// useDebounce.tsimport { useCallback, useEffect } from "react"/**
*
@param { (...args: any[]) => any } fn - A callback function to use debounce effect on.
*
@param { number } delay - A number that indicates how much time it waits.
*
@param { any[] } deps - A dependency list.
*
*/
export const useDebounce = (
fn: (...args: any[]) => any,
delay: number,
deps: any[]
) => {
/**
* Store the memoized version of the callback.
* It changes only when one of the dependencies has has changed.
* See official documentation at:
https://reactjs.org/docs/hooks-reference.html#usecallback
* */
const callback = useCallback(fn, deps)
/**
* useEffect gets re-called whenever "callback" changes.
* You can add "delay" to the second argument array,
* if you want to change "delay" dynamically.
* */
useEffect(() => {
// Call the memoized version of callback after the delay
const handler = setTimeout(() => {
callback()
}, delay)
/**
* Clear timeout when useEffect gets re-called,
* in other words, when "callback" changes.
* */
return () => {
clearTimeout(handler)
}
}, [callback])
}

I left some comments in the code to explain each line, but basically, all it does is execute the passed callback function if the user doesn’t type anything for a delay time.

9. Modify the app to use the “useDebounce" hook

Finally, we need to modify the app to use the useDebounce hook that we just created. Make the following changes to App.tsx.

// App.tsx
// Duplicates are omitted and replaced with "..."
import React, { useState } from 'react'// Change No.1
import { useDebounce } from './hooks/useDebounce'
...const App = () => {
...
const onChangeText = async () => {
if (search.term.trim() === '') return
if (!search.fetchPredictions) return
const apiUrl = `${GOOGLE_PACES_API_BASE_URL}/autocomplete/json?key=${env.GOOGLE_API_KEY}&input=${search.term}`
try {
const result = await axios.request({
method: 'post',
url: apiUrl
})
if (result) {
const { data: { predictions } } = result
setPredictions(predictions)
setShowPredictions(true)
}
} catch (e) {
console.log(e)
}
}
// Change No.2
useDebounce(onChangeText, 1000, [search.term])
...return (
<>
<StatusBar barStyle="dark-content" />
<SafeAreaView style={container}>
<View style={body}>
<SearchBarWithAutocomplete
value={search.term}
// Change No.3
onChangeText={(text) => {
setSearch({ term: text, fetchPredictions: true })
}}

showPredictions={showPredictions}
predictions={predictions}
onPredictionTapped={onPredictionTapped}
/>
</View>
</SafeAreaView>
</>
)
}
  1. Import useDebounce
  2. Add the line to call useDebounce right under onChangeText
    This gets called whenever it detects a change to search.term .
  3. Change the onChangeText prop for SearchBarWithAutocomplete
    Delete the line onChangeText() from the onChangeText prop since it is no longer needed.

Now, you shouldn’t see predictions until you stop typing for a second.

That’s it for this article.

You can find the completed code at my GitHub repository.

References

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store