Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .all-contributorsrc
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,16 @@
"contributions": [
"doc"
]
},
{
"login": "jacobknaack",
"name": "Jacob Knaack",
"avatar_url": "https://avatars.githubusercontent.com/u/15336054?v=4",
"profile": "https://github.com/JacobKnaack",
"contributions": [
"code",
"doc"
]
}
],
"contributorsPerLine": 7,
Expand Down
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,7 @@
node_modules
dist
coverage
.size-snapshot.json
.size-snapshot.json

# build artifacts
*.tgz
144 changes: 133 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,79 @@ const PlacesAutocomplete = () => {
};
```

## Now Supporting Places 2025

Please read the [API migration overview](https://developers.google.com/maps/documentation/places/web-service/legacy/migrate-overview) to get started, you must opt-in to the new API in order to use. Once activated you can opt-in to the new API features by specifying `useLegacy: false` in the hooks parameters:

```js
usePlacesAutocomplete({ useLegacy: false });
```

By default the new API returns a PlacePrediction, but you can easily convert these to the legacy prediction object:

```js
import usePlacesAutocomplete, {
getGeocode,
getLatLng,
getLegacyPrediction, // use to convert PlacePrediction to something similar to AutocompletePrediction
} from "use-places-autocomplete";
import useOnclickOutside from "react-cool-onclickoutside";

const PlacesAutocomplete = () => {
const {
ready,
value,
suggestions: { status, data },
setValue,
clearSuggestions,
} = usePlacesAutocomplete({
useLegacy: false, // Activate new API implementation
});
const ref = useOnclickOutside(() => {
clearSuggestions();
});

const handleInput = (e) => {
setValue(e.target.value);
};

const handleSelect =
({ description }) =>
() => {
setValue(description, false);
clearSuggestions();
};

const renderSuggestions = () =>
// apply utility function to map new API results for quick refactoring
data.map(getLegacyPrediction).map((suggestion) => {
const {
place_id,
structured_formatting: { main_text, secondary_text },
} = suggestion;

return (
<li key={place_id} onClick={handleSelect(suggestion)}>
<strong>{main_text}</strong> <small>{secondary_text}</small>
</li>
);
});

return (
<div ref={ref}>
<input
value={value}
onChange={handleInput}
disabled={!ready}
placeholder="Where are you going?"
/>
{/* We can use the "status" to decide whether we should display the dropdown or not */}
{status === "OK" && <ul>{renderSuggestions()}</ul>}
</div>
);
};
```

## Lazily Initializing The Hook

When loading the Google Maps Places API via a 3rd-party library, you may need to wait for the script to be ready before using this hook. However, you can lazily initialize the hook in the following ways, depending on your use case.
Expand Down Expand Up @@ -292,16 +365,16 @@ const returnObj = usePlacesAutocomplete(parameterObj);

When using `usePlacesAutocomplete`, you can configure the following options via the parameter.

| Key | Type | Default | Description |
| ---------------- | --------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `requestOptions` | object | | The [request options](https://developers.google.com/maps/documentation/javascript/reference/places-autocomplete-service#AutocompletionRequest) of Google Maps Places API except for `input` (e.g. bounds, radius, etc.).|
| `googleMaps` | object | `window.google.maps` | In case you want to provide your own Google Maps object, pass the `google.maps` to it. |
| `callbackName` | string | | The value of the `callback` parameter when [loading the Google Maps JavaScript library](#load-the-library). |
| `debounce` | number | `200` | Number of milliseconds to delay before making a request to Google Maps Places API. |
| `cache` | number \| false | `86400` (24 hours) | Number of seconds to [cache the response data of Google Maps Places API](#cache-data-for-you). |
| `cacheKey` | string | `"upa"` | Optional cache key so one can use multiple caches if needed. |
| `defaultValue` | string | `""` | Default value for the `input` element. |
| `initOnMount` | boolean | `true` | Initialize the hook with Google Maps Places API when the component mounts. |
| Key | Type | Default | Description |
| ---------------- | --------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `requestOptions` | object | | The [request options](https://developers.google.com/maps/documentation/javascript/reference/places-autocomplete-service#AutocompletionRequest) of Google Maps Places API except for `input` (e.g. bounds, radius, etc.). |
| `googleMaps` | object | `window.google.maps` | In case you want to provide your own Google Maps object, pass the `google.maps` to it. |
| `callbackName` | string | | The value of the `callback` parameter when [loading the Google Maps JavaScript library](#load-the-library). |
| `debounce` | number | `200` | Number of milliseconds to delay before making a request to Google Maps Places API. |
| `cache` | number \| false | `86400` (24 hours) | Number of seconds to [cache the response data of Google Maps Places API](#cache-data-for-you). |
| `cacheKey` | string | `"upa"` | Optional cache key so one can use multiple caches if needed. |
| `defaultValue` | string | `""` | Default value for the `input` element. |
| `initOnMount` | boolean | `true` | Initialize the hook with Google Maps Places API when the component mounts. |

### Return object

Expand Down Expand Up @@ -424,7 +497,7 @@ const PlacesAutocomplete = () => {

## Utility Functions

We provide [getGeocode](#getgeocode), [getLatLng](#getlatlng), [getZipCode](#getzipcode), and [getDetails](#getdetails) utils for you to do geocoding and get geographic coordinates when needed.
We provide [getGeocode](#getgeocode), [getLatLng](#getlatlng), [getZipCode](#getzipcode), and [getDetails](#getdetails) utils for you to do geocoding and get geographic coordinates when needed. We also now support [fetchFields](#fetchFields) from the new 2025 Places API.

### getGeocode

Expand Down Expand Up @@ -555,6 +628,55 @@ const PlacesAutocomplete = () => {
- `placeResult: object | null` - [the details](https://developers.google.com/maps/documentation/javascript/reference/places-service#PlaceResult) about the specific place your queried.
- `error: any` - an exception.

### fetchFields

Replaces getDetails when using Google Places 2025 updates. Retrieves information about a particular place ID.

```js
import usePlacesAutocomplete { fetchFields } from "use-places-autocomplete";

const AutocompleteFields = () => {
const { suggestions, value, setValue } = usePlacesAutocomplete({ useLegacy: false });

const handleChange = (e) => {
setValue(e.target.value);
};

const submit = () => {
const placePrediction = suggestions.data[0]

const parameter = {
placeId: placePrediction.placeId, // provided by PlacePrediction https://developers.google.com/maps/documentation/javascript/reference/autocomplete-data#PlacePrediction
// uses properties of the new Place class: https://developers.google.com/maps/documentation/javascript/reference/place#Place
fields: ["displayName", "formattedAddress"],
};

fetchFields(parameter)
.then(({ place }) => {
console.log("Name: ", place.displayName);
console.log("Address: ", place.formattedAddress);
})
.catch((error) => {
console.log("Error: ", error);
});
};

return (
<div>
<input value={value} onChange={handleChange} />
{/* Render dropdown */}
<button onClick={submit}>Submit Suggestion</button>
</div>
);
}
```

`fetchFields` is an asynchronous function with the following API:

- `parameter: object` - [the request](https://developers.google.com/maps/documentation/javascript/reference/place#FetchFieldsRequest) on the places `fetchFields()` method. You must supply a `fields` array using the Place properties you would like returned.
- `{ place: Place } | null` - [the place instance](https://developers.google.com/maps/documentation/javascript/reference/place#Place) with the properties requested.
- `error: any` - an exception.

> ⚠️ warning, you are billed based on how much information you retrieve, So it is advised that you retrieve just what you need.

## Articles / Blog Posts
Expand Down
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "use-places-autocomplete",
"version": "4.0.1",
"version": "4.1.0",
"description": "React hook for Google Maps Places Autocomplete.",
"license": "MIT",
"homepage": "https://use-places-autocomplete.netlify.app",
Expand Down Expand Up @@ -76,7 +76,7 @@
"@rollup/plugin-replace": "^3.0.1",
"@testing-library/react": "^12.1.2",
"@testing-library/react-hooks": "^7.0.2",
"@types/google.maps": "^3.49.2",
"@types/google.maps": "^3.58.1",
"@types/jest": "^27.4.0",
"@types/lodash.debounce": "^4.0.6",
"@types/react": "^17.0.38",
Expand All @@ -101,6 +101,7 @@
"typescript": "^4.5.4"
},
"peerDependencies": {
"react": ">= 16.8.0"
"react": ">= 16.8.0",
"react-dom": ">= 16.8.0"
}
}
42 changes: 42 additions & 0 deletions src/__tests__/createAutocompleteRequest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import createAutocompleteRequest from "../createAutocompleteRequest";

describe("toAutocompleteSuggestionInput", () => {
it("should convert requestOptions and input to AutocompleteSuggestionInput", () => {
// Mock sessionToken as an object, not a string
const mockSessionToken = {};
const requestOptions = {
radius: 500,
bounds: { east: 1, north: 2, south: 3, west: 4 },
offset: 2,
sessionToken: mockSessionToken,
types: ["address"],
componentRestrictions: { country: "us" },
};
const input = "pizza";

const result = createAutocompleteRequest(requestOptions, input);

expect(result).toEqual({
input: "pizza",
radius: 500,
bounds: { east: 1, north: 2, south: 3, west: 4 },
offset: 2,
sessionToken: mockSessionToken,
types: ["address"],
componentRestrictions: { country: "us" },
});
});

it("should handle missing optional fields", () => {
const requestOptions = {};
const input = "coffee";
const result = createAutocompleteRequest(requestOptions, input);
expect(result).toEqual({ input: "coffee" });
});

it("should handle undefined requestOptions", () => {
const input = "bar";
const result = createAutocompleteRequest(undefined, input);
expect(result).toEqual({ input: "bar" });
});
});
85 changes: 85 additions & 0 deletions src/__tests__/fetchAutocompleteSuggestions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import fetchAutocompleteSuggestions, {
getPlacePredictions,
} from "../fetchAutocompleteSuggestions";
import { getLegacyPrediction, getLegacyPredictionErr } from "../utils";

describe("fetchAutocompleteSuggestions", () => {
const mockResponse = {
suggestions: [
{
placeId: "test-place-id",
description: "test-description",
},
],
};

it("Should return an Autocomplete Suggestion response", async () => {
const mockRequest = { input: "test-input" };
const response = await fetchAutocompleteSuggestions(mockRequest, {
AutocompleteSuggestion: {
fetchAutocompleteSuggestions: jest.fn().mockResolvedValue(mockResponse),
},
});
expect(response).toEqual(mockResponse.suggestions);
});
it("Should throw an error if AutocompleteSuggestion API is not available", async () => {
const mockRequest = { input: "test-input" };
await expect(fetchAutocompleteSuggestions(mockRequest, {})).rejects.toThrow(
"Google Maps AutocompleteSuggestion API is not available. Make sure the Maps JavaScript API is loaded with the correct libraries."
);
});
it("Should throw an error if the API request fails", async () => {
const mockRequest = { input: "test-input" };
const mockApi = {
AutocompleteSuggestion: {
fetchAutocompleteSuggestions: jest
.fn()
.mockRejectedValue(new Error("API request failed")),
},
};
await expect(
fetchAutocompleteSuggestions(mockRequest, mockApi)
).rejects.toThrow("API request failed");
});
it("Should return a array of predictions", () => {
const suggestions = [
{
placePrediction: {
placeId: "1",
},
},
{
placePrediction: {
placeId: "2",
},
},
] as unknown as google.maps.places.AutocompleteSuggestion[];
const predictions = getPlacePredictions(suggestions);
expect(predictions).toEqual([{ placeId: "1" }, { placeId: "2" }]);
});
it("Should be able to extract place details from a PlacePrediction", () => {
const placePrediction = {
placeId: "test-place-id",
text: { text: "Test Place" },
mainText: { text: "Test Main Text" },
secondaryText: { text: "Test Secondary Text" },
} as unknown as google.maps.places.PlacePrediction;

const details = getLegacyPrediction(placePrediction);
expect(details).toEqual({
place_id: "test-place-id",
structured_formatting: {
main_text: "Test Main Text",
secondary_text: "Test Secondary Text",
},
description: "Test Place",
});
});
it("Should throw an error if PlacePrediction is invalid", () => {
const invalidPlacePrediction =
null as unknown as google.maps.places.PlacePrediction;
expect(() => getLegacyPrediction(invalidPlacePrediction)).toThrow(
getLegacyPredictionErr
);
});
});
40 changes: 40 additions & 0 deletions src/__tests__/fetchFields.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { fetchFields, fetchFieldsErr } from "../utils";

describe("Places 2025 API - fetchFields", () => {
const mockPlace = {
fetchFields: jest.fn().mockResolvedValue({
place: { displayName: "test-name" },
}),
};
beforeEach(() => {
jest.clearAllMocks();
console.error = jest.fn();
window.google = {
maps: {
places: {
// @ts-ignore
Place: jest.fn().mockImplementation(() => mockPlace),
},
},
};
});

it("Should fetch fields for a given placeId", async () => {
const args = {
placeId: "test-place-id",
fields: ["displayName"],
};
const result = await fetchFields(args);
expect(mockPlace.fetchFields).toHaveBeenCalledWith({
fields: args.fields,
});
expect(result).toEqual({ place: { displayName: "test-name" } });
});
it("Should throw an error if placeId is not provided", async () => {
const args = {
fields: ["displayName"],
};
// @ts-ignore
await expect(fetchFields(args)).rejects.toThrow(fetchFieldsErr);
});
});
Loading