places-autocomplete-hook
Version:
A React hook for Google Places Autocomplete API
168 lines • 5.34 kB
JavaScript
// src/index.ts
import { useState, useCallback, useEffect, useRef } from "react";
function usePlacesAutocomplete({
apiKey,
debounceMs = 300,
language = "en",
types,
sessionToken,
location,
setSelectedPlace
}) {
const [value, setValue] = useState("");
const [predictions, setPredictions] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const debounceTimer = useRef(null);
const clear = useCallback(() => {
setPredictions([]);
setError(null);
setValue("");
}, []);
const extractAddressComponent = (components, type) => {
const component = components.find((comp) => comp.types.includes(type));
return component?.longText;
};
const getPlaceDetails = useCallback(
async (placeId) => {
try {
const response = await fetch(
`https://places.googleapis.com/v1/places/${placeId}?key=${apiKey}&languageCode=${language}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
"X-Goog-FieldMask": "formattedAddress,addressComponents,location",
...sessionToken && { "X-Goog-Api-Key": apiKey }
}
}
);
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error?.message || `HTTP error! status: ${response.status}`);
}
const data = await response.json();
const addressComponents = data.addressComponents || [];
return {
placeId,
formattedAddress: data.formattedAddress,
addressComponents,
location: data.location,
streetNumber: extractAddressComponent(addressComponents, "street_number"),
streetName: extractAddressComponent(addressComponents, "route"),
city: extractAddressComponent(addressComponents, "locality"),
state: extractAddressComponent(addressComponents, "administrative_area_level_1"),
country: extractAddressComponent(addressComponents, "country"),
postalCode: extractAddressComponent(addressComponents, "postal_code")
};
} catch (err) {
throw err instanceof Error ? err : new Error("An error occurred while fetching place details");
}
},
[apiKey, language, sessionToken]
);
const handlePlaceSelect = useCallback(
async (placeId) => {
setSelectedPlace?.(placeId);
},
[setSelectedPlace]
);
const search = useCallback(
async (input) => {
if (!input.trim()) {
clear();
return;
}
try {
setLoading(true);
setError(null);
const requestBody = {
input,
languageCode: language
};
if (types) {
requestBody.types = types;
}
if (location) {
requestBody.locationBias = {
circle: {
center: {
latitude: location.lat,
longitude: location.lng
},
radius: location.radius
}
};
}
const response = await fetch(
`https://places.googleapis.com/v1/places:autocomplete?key=${apiKey}`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Goog-FieldMask": "suggestions.placePrediction.place,suggestions.placePrediction.placeId,suggestions.placePrediction.text,suggestions.placePrediction.structuredFormat,suggestions.placePrediction.types",
...sessionToken && { "X-Goog-Api-Key": apiKey }
},
body: JSON.stringify(requestBody)
}
);
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error?.message || `HTTP error! status: ${response.status}`);
}
const data = await response.json();
setPredictions(data.suggestions.map((suggestion) => suggestion.placePrediction) || []);
} catch (err) {
setError(err instanceof Error ? err : new Error("An error occurred"));
setPredictions([]);
} finally {
setLoading(false);
}
},
[apiKey, language, sessionToken, types, location, clear]
);
const debouncedSearch = useCallback(
async (input) => {
if (debounceTimer.current) {
clearTimeout(debounceTimer.current);
}
return new Promise((resolve) => {
debounceTimer.current = setTimeout(async () => {
await search(input);
resolve();
}, debounceMs);
});
},
[search, debounceMs]
);
useEffect(() => {
return () => {
if (debounceTimer.current) {
clearTimeout(debounceTimer.current);
}
};
}, []);
return {
value,
suggestions: {
status: error ? "ERROR" : loading ? "LOADING" : predictions.length > 0 ? "OK" : "ZERO_RESULTS",
data: predictions
},
setValue: (newValue, shouldFetchData = true) => {
setValue(newValue);
if (shouldFetchData) {
debouncedSearch(newValue);
}
},
clearSuggestions: clear,
search: debouncedSearch,
loading,
error,
getPlaceDetails,
handlePlaceSelect
};
}
export {
usePlacesAutocomplete
};
//# sourceMappingURL=index.mjs.map