@gralweb/use-google-places-autocomplete
v1.1.3
Published
Headless React hook for Google Places Autocomplete with full TypeScript support. Bring your own UI.
Maintainers
Readme
use-google-places-autocomplete
Headless React hook for Google Places Autocomplete. Bring your own UI.
Why Headless?
This package provides only the logic for Google Places Autocomplete, giving you complete freedom to build your own UI:
- ✅ No CSS to override - Use your own styles
- ✅ Works with any UI library - shadcn, MUI, Chakra, Tailwind, etc.
- ✅ Smaller bundle size - Only the hook logic
- ✅ Full TypeScript support - Complete type safety
- ✅ Modern Google Places API - Uses latest API features
Installation
npm install @gralweb/use-google-places-autocomplete
# or
yarn add @gralweb/use-google-places-autocomplete
# or
pnpm add @gralweb/use-google-places-autocompleteQuick Start
import { usePlacesAutocomplete } from '@gralweb/use-google-places-autocomplete'
function MyAutocomplete() {
const {
getInputProps,
containerRef,
predictions,
isOpen,
handleSelectPlace,
isLoaded
} = usePlacesAutocomplete({
apiKey: 'YOUR_GOOGLE_MAPS_API_KEY',
onPlaceSelect: (place) => {
console.log('Selected place:', place)
},
})
if (!isLoaded) return <div>Loading...</div>
return (
<div ref={containerRef}>
<input {...getInputProps({ placeholder: "Search places..." })} />
{isOpen && predictions.length > 0 && (
<ul>
{predictions.map((prediction) => (
<li
key={prediction.placeId}
onClick={() => handleSelectPlace(prediction)}
>
{prediction.description}
</li>
))}
</ul>
)}
</div>
)
}API Reference
usePlacesAutocomplete(options)
Options
| Option | Type | Required | Default | Description |
| ------ | ---- | -------- | ------- | ----------- |
| apiKey | string | ✅ | - | Google Maps API Key |
| onPlaceSelect | (place: PlaceDetails) => void | ❌ | - | Callback when a place is selected |
| onError | (error: Error) => void | ❌ | - | Error handler callback |
| options | AutocompleteOptions | ❌ | See below | Google Places API options |
| debounceMs | number | ❌ | 300 | Debounce delay in milliseconds |
| minChars | number | ❌ | 3 | Minimum characters to trigger search |
Default options:
{
types: ['geocode'],
fields: [
'id',
'displayName',
'formattedAddress',
'location',
'addressComponents',
],
}Returns
{
// Function to get input props with automatic merging (recommended)
getInputProps: (userProps?: React.InputHTMLAttributes<HTMLInputElement>) =>
React.InputHTMLAttributes<HTMLInputElement> & { ref: React.RefObject<HTMLInputElement> }
// Legacy: Props to spread on your input element (deprecated, use getInputProps instead)
inputProps: {
ref: React.RefObject<HTMLInputElement>
value: string
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void
onKeyDown: (e: React.KeyboardEvent<HTMLInputElement>) => void
autoComplete: string
}
// Ref for the container (handles click-outside)
containerRef: React.RefObject<HTMLDivElement | null>
// Predictions array
predictions: PlaceResult[]
// UI state
isOpen: boolean
selectedIndex: number
// Selection handler
handleSelectPlace: (prediction: PlaceResult) => void
// Loading state
isLoaded: boolean
loadError: Error | null
// Manual control methods
clearSuggestions: () => void
setValue: (value: string) => void
}Examples
Controlled Input (Forms)
New in v1.1.0: Use getInputProps() for seamless integration with controlled inputs and form libraries.
import { useState } from 'react'
import { usePlacesAutocomplete } from '@gralweb/use-google-places-autocomplete'
function ControlledExample() {
const [address, setAddress] = useState('')
const {
getInputProps,
containerRef,
predictions,
isOpen,
handleSelectPlace
} = usePlacesAutocomplete({
apiKey: process.env.VITE_GOOGLE_MAPS_API_KEY,
onPlaceSelect: (place) => {
setAddress(place.formattedAddress)
},
})
return (
<div ref={containerRef}>
<input
{...getInputProps({
value: address,
onChange: (e) => setAddress(e.target.value),
placeholder: "Enter your address...",
className: "form-input"
})}
/>
{isOpen && predictions.length > 0 && (
<ul className="suggestions">
{predictions.map((prediction) => (
<li
key={prediction.placeId}
onClick={() => handleSelectPlace(prediction)}
>
<div>{prediction.mainText}</div>
<div className="text-sm text-gray-500">{prediction.secondaryText}</div>
</li>
))}
</ul>
)}
</div>
)
}With React Hook Form
import { useForm } from 'react-hook-form'
import { usePlacesAutocomplete } from '@gralweb/use-google-places-autocomplete'
function FormExample() {
const { register, handleSubmit, setValue, watch } = useForm()
const address = watch('address')
const { getInputProps, containerRef, predictions, isOpen, handleSelectPlace } =
usePlacesAutocomplete({
apiKey: process.env.VITE_GOOGLE_MAPS_API_KEY,
onPlaceSelect: (place) => {
setValue('address', place.formattedAddress)
setValue('lat', place.geometry?.location.lat)
setValue('lng', place.geometry?.location.lng)
},
})
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div ref={containerRef}>
<input
{...getInputProps({
...register('address'),
value: address,
placeholder: "Address"
})}
/>
{isOpen && predictions.length > 0 && (
<ul>
{predictions.map((prediction) => (
<li key={prediction.placeId} onClick={() => handleSelectPlace(prediction)}>
{prediction.description}
</li>
))}
</ul>
)}
</div>
<button type="submit">Submit</button>
</form>
)
}With Custom Styling
import { usePlacesAutocomplete } from '@gralweb/use-google-places-autocomplete'
function StyledAutocomplete() {
const {
getInputProps,
containerRef,
predictions,
isOpen,
selectedIndex,
handleSelectPlace
} = usePlacesAutocomplete({
apiKey: process.env.VITE_GOOGLE_MAPS_API_KEY,
onPlaceSelect: (place) => {
console.log('Selected:', place.formattedAddress)
},
})
return (
<div ref={containerRef} className="relative w-full max-w-md">
<input
{...getInputProps({
className: "w-full px-4 py-2 border rounded-lg",
placeholder: "Search for a place..."
})}
/>
{isOpen && predictions.length > 0 && (
<ul className="absolute w-full mt-1 bg-white border rounded-lg shadow-lg">
{predictions.map((prediction, index) => (
<li
key={prediction.placeId}
onClick={() => handleSelectPlace(prediction)}
className={`px-4 py-2 cursor-pointer hover:bg-gray-100 ${
index === selectedIndex ? 'bg-gray-100' : ''
}`}
>
<div className="font-medium">{prediction.mainText}</div>
<div className="text-sm text-gray-500">{prediction.secondaryText}</div>
</li>
))}
</ul>
)}
</div>
)
}With shadcn/ui
import { usePlacesAutocomplete } from '@gralweb/use-google-places-autocomplete'
import { Input } from '@/components/ui/input'
import { Card } from '@/components/ui/card'
function ShadcnExample() {
const {
getInputProps,
containerRef,
predictions,
isOpen,
handleSelectPlace
} = usePlacesAutocomplete({
apiKey: process.env.VITE_GOOGLE_MAPS_API_KEY,
onPlaceSelect: (place) => console.log(place),
})
return (
<div ref={containerRef} className="relative w-full max-w-md">
<Input {...getInputProps({ placeholder: "Search places..." })} />
{isOpen && predictions.length > 0 && (
<Card className="absolute w-full mt-1 p-0">
{predictions.map((prediction) => (
<div
key={prediction.placeId}
onClick={() => handleSelectPlace(prediction)}
className="px-4 py-2 cursor-pointer hover:bg-accent"
>
{prediction.description}
</div>
))}
</Card>
)}
</div>
)
}With Material-UI
import { usePlacesAutocomplete } from '@gralweb/use-google-places-autocomplete'
import TextField from '@mui/material/TextField'
import List from '@mui/material/List'
import ListItem from '@mui/material/ListItem'
import Paper from '@mui/material/Paper'
function MUIExample() {
const {
getInputProps,
containerRef,
predictions,
isOpen,
handleSelectPlace
} = usePlacesAutocomplete({
apiKey: process.env.VITE_GOOGLE_MAPS_API_KEY,
})
return (
<div ref={containerRef}>
<TextField
{...getInputProps({
label: "Search places",
variant: "outlined"
})}
fullWidth
/>
{isOpen && predictions.length > 0 && (
<Paper elevation={3}>
<List>
{predictions.map((prediction) => (
<ListItem
key={prediction.placeId}
button
onClick={() => handleSelectPlace(prediction)}
>
{prediction.description}
</ListItem>
))}
</List>
</Paper>
)}
</div>
)
}With Manual Control
import { usePlacesAutocomplete } from '@gralweb/use-google-places-autocomplete'
function AdvancedExample() {
const {
getInputProps,
containerRef,
predictions,
isOpen,
handleSelectPlace,
clearSuggestions,
setValue,
isLoaded,
loadError
} = usePlacesAutocomplete({
apiKey: process.env.VITE_GOOGLE_MAPS_API_KEY,
onPlaceSelect: (place) => {
console.log('Selected:', place)
setTimeout(() => clearSuggestions(), 100)
},
onError: (error) => {
console.error('Error:', error)
},
})
const handleClear = () => {
setValue('')
clearSuggestions()
}
if (loadError) {
return <div>Error: {loadError.message}</div>
}
if (!isLoaded) {
return <div>Loading Google Maps...</div>
}
return (
<div ref={containerRef} className="relative">
<div className="flex gap-2">
<input {...getInputProps({ className: "flex-1" })} />
<button onClick={handleClear}>Clear</button>
</div>
{isOpen && predictions.length > 0 && (
<ul>
{predictions.map((prediction) => (
<li
key={prediction.placeId}
onClick={() => handleSelectPlace(prediction)}
>
{prediction.description}
</li>
))}
</ul>
)}
</div>
)
}Restrict by Country
const { getInputProps, containerRef, predictions, isOpen } = usePlacesAutocomplete({
apiKey: process.env.VITE_GOOGLE_MAPS_API_KEY,
options: {
componentRestrictions: { country: 'us' },
},
})Filter by Type
const { getInputProps, containerRef, predictions, isOpen } = usePlacesAutocomplete({
apiKey: process.env.VITE_GOOGLE_MAPS_API_KEY,
options: {
types: ['restaurant', 'cafe'],
},
})TypeScript
Full TypeScript support with exported types:
import type {
PlaceDetails,
PlaceResult,
AutocompleteOptions,
UsePlacesAutocompleteOptions,
UsePlacesAutocompleteReturn
} from '@gralweb/use-google-places-autocomplete'
const handleSelect = (place: PlaceDetails) => {
console.log(place.formattedAddress)
console.log(place.geometry?.location.lat)
console.log(place.geometry?.location.lng)
}Advanced Usage
Using Helpers Directly
import { getPredictions, getPlaceDetails } from '@gralweb/use-google-places-autocomplete'
// Get predictions manually
const predictions = await getPredictions('New York', {
sessionToken: myToken,
options: { types: ['geocode'] }
})
// Get place details manually
const details = await getPlaceDetails('ChIJOwg_06VPwokRYv534QaPC8g', {
fields: ['formattedAddress', 'location']
})Using Constants
import {
DEFAULT_AUTOCOMPLETE_OPTIONS,
DEFAULT_DEBOUNCE_MS,
DEFAULT_MIN_CHARS
} from '@gralweb/use-google-places-autocomplete'
console.log(DEFAULT_DEBOUNCE_MS) // 300
console.log(DEFAULT_MIN_CHARS) // 3Getting a Google Maps API Key
- Go to Google Cloud Console
- Create a project or select an existing one
- Enable Places API (New)
- Create credentials (API Key)
- (Optional) Restrict the API key to your domain
⚠️ Important Legal Notice
This package is a wrapper around the Google Maps Places API. Users of this package must:
- Have a valid Google Maps API Key
- Comply with Google Maps Platform Terms of Service
- Follow Google Maps Platform Usage Limits
- Display the Google logo and attributions as required by Google's terms
- Be responsible for any API usage fees incurred
This package and its author are not affiliated with, endorsed by, or sponsored by Google LLC.
Browser Support
- Chrome (latest)
- Firefox (latest)
- Safari (latest)
- Edge (latest)
License
MIT License - see LICENSE file for details.
Disclaimer
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Users are solely responsible for:
- Complying with Google Maps Platform Terms of Service
- Any applicable usage fees from Google
- Proper implementation and security of API keys
- Meeting Google's attribution requirements
Contributing
Contributions are welcome! Please open an issue or pull request.
