(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 usingnpx react-native run-ios
, it might not work. There seems to be a problem with theFlipper
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 withJAVA_HOME
path configuration. In order to make it work, I uninstalled the old version of Java (version 9) and installedjEnv
and some Java versions fromAdoptOpenJDK
viaHomebrew
.
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) : stylereturn (
<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.
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”.
This will generate an API key, so keep it somewhere safe because you need it later to use the API in the app.
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.
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.
- Import
axios
- Store base URL in a constant so it can be re-used
- Define
PredictionType
following the official API docs from Google - Add
fetchPredictions
to thesearch
statefetchPredictions
is used to decide if the app should send a request to the API. - Add a new state
predictions
: Predictions to display 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 iffetchPredictions
is set to befalse
. This line is needed to prevent the app from sending a request when we update the text in the search bar on prediction tapped.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.- 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
}
- Import
PredictionType
fromApp.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 namedtypes.ts
and store type definitions there. - 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.
- Import
useState
fromreact
- Import
FlatList
,TouchableOpacity
andText
fromreact-native
- Add the new state
inputSize
is used to match the prediction box width and the
inputSizeTextInput
width. WhenTextInput
is rendered, its width and height will be stored in this state viaonLayout
prop. - Extract new props
- Adjust the input style
Adjust the input style when predictions are being displayed. Otherwise, there will be unwanted spaces between theTextInput
and the predictions because ofborderBottomRadius
. _renderPredictions
method
The_renderPredictions
method is used to render predictions right belowTextInput
.- Apply the input style change
- Set
inputSize
viaonLayout
- Render predictions when
showPredictions
istrue
- 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>
</>
)
}
- Import
useDebounce
- Add the line to call
useDebounce
right underonChangeText
This gets called whenever it detects a change tosearch.term
. - Change the
onChangeText
prop forSearchBarWithAutocomplete
Delete the lineonChangeText()
from theonChangeText
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
- Lodash official documentation — _.debounce:
https://lodash.com/docs/4.17.15#debounce - Debounce in JavaScript — Improve Your Application’s Performance:
https://levelup.gitconnected.com/debounce-in-javascript-improve-your-applications-performance-5b01855e086 - Debounce Explained — How to Make Your JavaScript Wait For Your User To Finish Typing:
https://www.freecodecamp.org/news/debounce-explained-how-to-make-your-javascript-wait-for-your-user-to-finish-typing-2/ - stackoverflow — How to use throttle or debounce with React Hook?:
https://stackoverflow.com/questions/54666401/how-to-use-throttle-or-debounce-with-react-hook - GitHub Issue — Cant build react native from box in XCode: ‘event2/event-config.h’ file not found #30836:
https://github.com/facebook/react-native/issues/30836 - Wrong JAVA_HOME after upgrade to macOS Big Sur v11.0.1:
https://stackoverflow.com/questions/64917779/wrong-java-home-after-upgrade-to-macos-big-sur-v11-0-1 - How to Uninstall Java on MacOS:
https://explainjava.com/uninstall-java-macos/ - Installing Java on MacOS using Homebrew and JEnv:
https://dev.to/gabethere/installing-java-on-a-mac-using-homebrew-and-jevn-12m8 - GitHub Repository — AdoptOpenJDK/homebrew-openjdk:
https://github.com/AdoptOpenJDK/homebrew-openjdk - GitHub Repository — jenv/jenv:
https://github.com/jenv/jenv