0
# Places Integration
1
2
Components for integrating Google Places API functionality including autocomplete, place search capabilities, and location-based services with comprehensive place data access.
3
4
## Capabilities
5
6
### Autocomplete Component
7
8
Provides place autocomplete functionality for input fields with extensive filtering and customization options.
9
10
```typescript { .api }
11
/**
12
* Provides place autocomplete functionality for input fields
13
* Enhances text inputs with intelligent place suggestions and validation
14
*/
15
interface AutocompleteProps {
16
children: React.ReactNode; // Required - must contain exactly one input element
17
bounds?: google.maps.LatLngBounds | google.maps.LatLngBoundsLiteral;
18
restrictions?: google.maps.places.ComponentRestrictions;
19
fields?: string[];
20
options?: google.maps.places.AutocompleteOptions;
21
types?: string[];
22
23
// Event handlers
24
onPlaceChanged?: () => void;
25
26
// Lifecycle events
27
onLoad?: (autocomplete: google.maps.places.Autocomplete) => void;
28
onUnmount?: (autocomplete: google.maps.places.Autocomplete) => void;
29
}
30
31
interface google.maps.places.AutocompleteOptions {
32
bounds?: google.maps.LatLngBounds | google.maps.LatLngBoundsLiteral;
33
componentRestrictions?: google.maps.places.ComponentRestrictions;
34
fields?: string[];
35
placeIdOnly?: boolean;
36
strictBounds?: boolean;
37
types?: string[];
38
}
39
40
interface google.maps.places.ComponentRestrictions {
41
country?: string | string[];
42
}
43
44
function Autocomplete(props: AutocompleteProps): JSX.Element;
45
```
46
47
**Usage Examples:**
48
49
```typescript
50
import React, { useState, useRef } from 'react';
51
import { GoogleMap, LoadScript, Autocomplete } from '@react-google-maps/api';
52
53
// Basic autocomplete
54
function BasicAutocomplete() {
55
const [selectedPlace, setSelectedPlace] = useState<google.maps.places.PlaceResult | null>(null);
56
const autocompleteRef = useRef<google.maps.places.Autocomplete | null>(null);
57
58
const onLoad = (autocomplete: google.maps.places.Autocomplete) => {
59
autocompleteRef.current = autocomplete;
60
};
61
62
const onPlaceChanged = () => {
63
if (autocompleteRef.current) {
64
const place = autocompleteRef.current.getPlace();
65
setSelectedPlace(place);
66
console.log('Selected place:', place);
67
}
68
};
69
70
return (
71
<LoadScript googleMapsApiKey="YOUR_API_KEY" libraries={['places']}>
72
<div>
73
<div style={{ padding: '10px' }}>
74
<Autocomplete onLoad={onLoad} onPlaceChanged={onPlaceChanged}>
75
<input
76
type="text"
77
placeholder="Enter a place"
78
style={{
79
boxSizing: 'border-box',
80
border: '1px solid transparent',
81
width: '240px',
82
height: '32px',
83
padding: '0 12px',
84
borderRadius: '3px',
85
boxShadow: '0 2px 6px rgba(0, 0, 0, 0.3)',
86
fontSize: '14px',
87
outline: 'none'
88
}}
89
/>
90
</Autocomplete>
91
</div>
92
93
{selectedPlace && (
94
<div style={{ padding: '10px', background: '#f0f0f0' }}>
95
<h4>Selected Place:</h4>
96
<div>Name: {selectedPlace.name}</div>
97
<div>Address: {selectedPlace.formatted_address}</div>
98
{selectedPlace.geometry && (
99
<div>
100
Coordinates: {selectedPlace.geometry.location?.lat()}, {selectedPlace.geometry.location?.lng()}
101
</div>
102
)}
103
</div>
104
)}
105
106
<GoogleMap
107
center={
108
selectedPlace?.geometry?.location
109
? selectedPlace.geometry.location.toJSON()
110
: { lat: 40.7128, lng: -74.0060 }
111
}
112
zoom={15}
113
mapContainerStyle={{ width: '100%', height: '400px' }}
114
/>
115
</div>
116
</LoadScript>
117
);
118
}
119
120
// Advanced autocomplete with restrictions and fields
121
function AdvancedAutocomplete() {
122
const [selectedPlace, setSelectedPlace] = useState<google.maps.places.PlaceResult | null>(null);
123
const [country, setCountry] = useState<string>('us');
124
const [placeType, setPlaceType] = useState<string>('establishment');
125
const autocompleteRef = useRef<google.maps.places.Autocomplete | null>(null);
126
127
// Specify which place data fields to retrieve
128
const placeFields = [
129
'place_id',
130
'name',
131
'formatted_address',
132
'geometry',
133
'types',
134
'rating',
135
'price_level',
136
'opening_hours',
137
'photos',
138
'international_phone_number',
139
'website'
140
];
141
142
const placeTypes = [
143
{ value: 'establishment', label: 'Establishments' },
144
{ value: 'geocode', label: 'Geocodes' },
145
{ value: 'address', label: 'Addresses' },
146
{ value: '(cities)', label: 'Cities' },
147
{ value: '(regions)', label: 'Regions' }
148
];
149
150
const onLoad = (autocomplete: google.maps.places.Autocomplete) => {
151
autocompleteRef.current = autocomplete;
152
};
153
154
const onPlaceChanged = () => {
155
if (autocompleteRef.current) {
156
const place = autocompleteRef.current.getPlace();
157
setSelectedPlace(place);
158
console.log('Place details:', place);
159
}
160
};
161
162
return (
163
<LoadScript googleMapsApiKey="YOUR_API_KEY" libraries={['places']}>
164
<div>
165
<div style={{ padding: '10px', background: '#f0f0f0' }}>
166
<div style={{ marginBottom: '10px' }}>
167
<label style={{ marginRight: '10px' }}>
168
Country:
169
<select
170
value={country}
171
onChange={(e) => setCountry(e.target.value)}
172
style={{ marginLeft: '5px' }}
173
>
174
<option value="us">United States</option>
175
<option value="ca">Canada</option>
176
<option value="gb">United Kingdom</option>
177
<option value="au">Australia</option>
178
<option value="de">Germany</option>
179
<option value="fr">France</option>
180
</select>
181
</label>
182
183
<label>
184
Place Type:
185
<select
186
value={placeType}
187
onChange={(e) => setPlaceType(e.target.value)}
188
style={{ marginLeft: '5px' }}
189
>
190
{placeTypes.map(type => (
191
<option key={type.value} value={type.value}>
192
{type.label}
193
</option>
194
))}
195
</select>
196
</label>
197
</div>
198
199
<Autocomplete
200
onLoad={onLoad}
201
onPlaceChanged={onPlaceChanged}
202
restrictions={{ country }}
203
types={[placeType]}
204
fields={placeFields}
205
options={{
206
strictBounds: false,
207
componentRestrictions: { country }
208
}}
209
>
210
<input
211
type="text"
212
placeholder={`Search for ${placeTypes.find(t => t.value === placeType)?.label.toLowerCase()} in ${country.toUpperCase()}`}
213
style={{
214
width: '100%',
215
height: '40px',
216
padding: '0 15px',
217
fontSize: '16px',
218
border: '2px solid #4285f4',
219
borderRadius: '8px',
220
outline: 'none',
221
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)'
222
}}
223
/>
224
</Autocomplete>
225
</div>
226
227
{selectedPlace && (
228
<div style={{ padding: '15px', background: 'white', margin: '10px', borderRadius: '8px', boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)' }}>
229
<h4>{selectedPlace.name}</h4>
230
<div><strong>Address:</strong> {selectedPlace.formatted_address}</div>
231
<div><strong>Types:</strong> {selectedPlace.types?.join(', ')}</div>
232
233
{selectedPlace.rating && (
234
<div><strong>Rating:</strong> {selectedPlace.rating} ⭐</div>
235
)}
236
237
{selectedPlace.price_level !== undefined && (
238
<div><strong>Price Level:</strong> {'$'.repeat(selectedPlace.price_level + 1)}</div>
239
)}
240
241
{selectedPlace.opening_hours && (
242
<div><strong>Open Now:</strong> {selectedPlace.opening_hours.open_now ? 'Yes' : 'No'}</div>
243
)}
244
245
{selectedPlace.international_phone_number && (
246
<div><strong>Phone:</strong> {selectedPlace.international_phone_number}</div>
247
)}
248
249
{selectedPlace.website && (
250
<div>
251
<strong>Website:</strong>
252
<a href={selectedPlace.website} target="_blank" rel="noopener noreferrer" style={{ marginLeft: '5px' }}>
253
{selectedPlace.website}
254
</a>
255
</div>
256
)}
257
258
{selectedPlace.photos && selectedPlace.photos.length > 0 && (
259
<div style={{ marginTop: '10px' }}>
260
<strong>Photos:</strong>
261
<div style={{ display: 'flex', gap: '10px', marginTop: '5px', overflow: 'auto' }}>
262
{selectedPlace.photos.slice(0, 3).map((photo, index) => (
263
<img
264
key={index}
265
src={photo.getUrl({ maxWidth: 200, maxHeight: 150 })}
266
alt={`${selectedPlace.name} photo ${index + 1}`}
267
style={{ borderRadius: '4px', flexShrink: 0 }}
268
/>
269
))}
270
</div>
271
</div>
272
)}
273
</div>
274
)}
275
276
<GoogleMap
277
center={
278
selectedPlace?.geometry?.location
279
? selectedPlace.geometry.location.toJSON()
280
: { lat: 40.7128, lng: -74.0060 }
281
}
282
zoom={selectedPlace ? 16 : 10}
283
mapContainerStyle={{ width: '100%', height: '400px' }}
284
>
285
{selectedPlace && selectedPlace.geometry && (
286
<Marker position={selectedPlace.geometry.location!.toJSON()} />
287
)}
288
</GoogleMap>
289
</div>
290
</LoadScript>
291
);
292
}
293
294
// Autocomplete with bounds restriction
295
function BoundedAutocomplete() {
296
const [mapCenter, setMapCenter] = useState({ lat: 40.7128, lng: -74.0060 });
297
const [mapBounds, setMapBounds] = useState<google.maps.LatLngBounds | null>(null);
298
const [selectedPlace, setSelectedPlace] = useState<google.maps.places.PlaceResult | null>(null);
299
const autocompleteRef = useRef<google.maps.places.Autocomplete | null>(null);
300
const mapRef = useRef<google.maps.Map | null>(null);
301
302
const onMapLoad = (map: google.maps.Map) => {
303
mapRef.current = map;
304
updateBounds(map);
305
};
306
307
const updateBounds = (map: google.maps.Map) => {
308
const bounds = map.getBounds();
309
if (bounds) {
310
setMapBounds(bounds);
311
}
312
};
313
314
const onPlaceChanged = () => {
315
if (autocompleteRef.current) {
316
const place = autocompleteRef.current.getPlace();
317
setSelectedPlace(place);
318
319
if (place.geometry && place.geometry.location) {
320
const location = place.geometry.location.toJSON();
321
setMapCenter(location);
322
323
// Fit map to place if it has a viewport
324
if (place.geometry.viewport && mapRef.current) {
325
mapRef.current.fitBounds(place.geometry.viewport);
326
}
327
}
328
}
329
};
330
331
return (
332
<LoadScript googleMapsApiKey="YOUR_API_KEY" libraries={['places']}>
333
<div>
334
<div style={{ padding: '10px', background: '#f0f0f0' }}>
335
<div style={{ marginBottom: '10px' }}>
336
<strong>Search within current map area:</strong>
337
</div>
338
339
<Autocomplete
340
onLoad={(autocomplete) => {
341
autocompleteRef.current = autocomplete;
342
}}
343
onPlaceChanged={onPlaceChanged}
344
bounds={mapBounds || undefined}
345
options={{
346
strictBounds: true // Restrict results to the specified bounds
347
}}
348
>
349
<input
350
type="text"
351
placeholder="Search places within map area"
352
style={{
353
width: '100%',
354
height: '40px',
355
padding: '0 15px',
356
fontSize: '16px',
357
border: '1px solid #ccc',
358
borderRadius: '4px'
359
}}
360
/>
361
</Autocomplete>
362
363
{selectedPlace && (
364
<div style={{ marginTop: '10px', padding: '10px', background: 'white', borderRadius: '4px' }}>
365
<strong>{selectedPlace.name}</strong>
366
<div>{selectedPlace.formatted_address}</div>
367
</div>
368
)}
369
</div>
370
371
<GoogleMap
372
center={mapCenter}
373
zoom={13}
374
mapContainerStyle={{ width: '100%', height: '400px' }}
375
onLoad={onMapLoad}
376
onBoundsChanged={() => {
377
if (mapRef.current) {
378
updateBounds(mapRef.current);
379
}
380
}}
381
>
382
{selectedPlace && selectedPlace.geometry && (
383
<Marker position={selectedPlace.geometry.location!.toJSON()} />
384
)}
385
</GoogleMap>
386
</div>
387
</LoadScript>
388
);
389
}
390
```
391
392
### StandaloneSearchBox Component
393
394
Provides place search functionality without the autocomplete dropdown, useful for custom search interfaces.
395
396
```typescript { .api }
397
/**
398
* Provides place search functionality without autocomplete dropdown
399
* Useful for custom search interfaces and batch place searching
400
*/
401
interface StandaloneSearchBoxProps {
402
children: React.ReactNode; // Required - must contain exactly one input element
403
bounds?: google.maps.LatLngBounds | google.maps.LatLngBoundsLiteral;
404
options?: google.maps.places.SearchBoxOptions;
405
406
// Event handlers
407
onPlacesChanged?: () => void;
408
409
// Lifecycle events
410
onLoad?: (searchBox: google.maps.places.SearchBox) => void;
411
onUnmount?: (searchBox: google.maps.places.SearchBox) => void;
412
}
413
414
interface google.maps.places.SearchBoxOptions {
415
bounds?: google.maps.LatLngBounds | google.maps.LatLngBoundsLiteral;
416
}
417
418
function StandaloneSearchBox(props: StandaloneSearchBoxProps): JSX.Element;
419
```
420
421
**Usage Examples:**
422
423
```typescript
424
import React, { useState, useRef } from 'react';
425
import { GoogleMap, LoadScript, StandaloneSearchBox, Marker } from '@react-google-maps/api';
426
427
// Basic search box
428
function BasicSearchBox() {
429
const [places, setPlaces] = useState<google.maps.places.PlaceResult[]>([]);
430
const [mapCenter, setMapCenter] = useState({ lat: 40.7128, lng: -74.0060 });
431
const searchBoxRef = useRef<google.maps.places.SearchBox | null>(null);
432
433
const onLoad = (searchBox: google.maps.places.SearchBox) => {
434
searchBoxRef.current = searchBox;
435
};
436
437
const onPlacesChanged = () => {
438
if (searchBoxRef.current) {
439
const places = searchBoxRef.current.getPlaces();
440
441
if (places && places.length > 0) {
442
setPlaces(places);
443
444
// Center map on first result
445
const firstPlace = places[0];
446
if (firstPlace.geometry && firstPlace.geometry.location) {
447
setMapCenter(firstPlace.geometry.location.toJSON());
448
}
449
450
console.log('Search results:', places);
451
}
452
}
453
};
454
455
return (
456
<LoadScript googleMapsApiKey="YOUR_API_KEY" libraries={['places']}>
457
<div>
458
<div style={{ padding: '10px' }}>
459
<StandaloneSearchBox onLoad={onLoad} onPlacesChanged={onPlacesChanged}>
460
<input
461
type="text"
462
placeholder="Search for places"
463
style={{
464
width: '100%',
465
height: '40px',
466
padding: '0 15px',
467
fontSize: '16px',
468
border: '2px solid #4285f4',
469
borderRadius: '8px',
470
outline: 'none'
471
}}
472
/>
473
</StandaloneSearchBox>
474
</div>
475
476
{places.length > 0 && (
477
<div style={{ padding: '10px', background: '#f0f0f0' }}>
478
<h4>Search Results ({places.length}):</h4>
479
<div style={{ maxHeight: '200px', overflowY: 'auto' }}>
480
{places.map((place, index) => (
481
<div key={index} style={{ padding: '5px', borderBottom: '1px solid #ccc' }}>
482
<strong>{place.name}</strong>
483
<div>{place.formatted_address}</div>
484
</div>
485
))}
486
</div>
487
</div>
488
)}
489
490
<GoogleMap
491
center={mapCenter}
492
zoom={15}
493
mapContainerStyle={{ width: '100%', height: '400px' }}
494
>
495
{places.map((place, index) =>
496
place.geometry && place.geometry.location && (
497
<Marker
498
key={index}
499
position={place.geometry.location.toJSON()}
500
title={place.name}
501
/>
502
)
503
)}
504
</GoogleMap>
505
</div>
506
</LoadScript>
507
);
508
}
509
510
// Advanced search with filtering and categorization
511
function AdvancedSearchBox() {
512
const [places, setPlaces] = useState<google.maps.places.PlaceResult[]>([]);
513
const [filteredPlaces, setFilteredPlaces] = useState<google.maps.places.PlaceResult[]>([]);
514
const [selectedCategory, setSelectedCategory] = useState<string>('all');
515
const [mapBounds, setMapBounds] = useState<google.maps.LatLngBounds | null>(null);
516
const searchBoxRef = useRef<google.maps.places.SearchBox | null>(null);
517
const mapRef = useRef<google.maps.Map | null>(null);
518
519
const categories = [
520
{ value: 'all', label: 'All Places' },
521
{ value: 'restaurant', label: 'Restaurants' },
522
{ value: 'lodging', label: 'Hotels' },
523
{ value: 'tourist_attraction', label: 'Attractions' },
524
{ value: 'store', label: 'Stores' },
525
{ value: 'bank', label: 'Banks' },
526
{ value: 'hospital', label: 'Hospitals' }
527
];
528
529
const onPlacesChanged = () => {
530
if (searchBoxRef.current) {
531
const searchResults = searchBoxRef.current.getPlaces();
532
533
if (searchResults && searchResults.length > 0) {
534
setPlaces(searchResults);
535
filterPlaces(searchResults, selectedCategory);
536
537
// Fit map to show all results
538
if (mapRef.current && searchResults.length > 1) {
539
const bounds = new google.maps.LatLngBounds();
540
searchResults.forEach(place => {
541
if (place.geometry && place.geometry.location) {
542
bounds.extend(place.geometry.location);
543
}
544
});
545
mapRef.current.fitBounds(bounds);
546
}
547
}
548
}
549
};
550
551
const filterPlaces = (placesToFilter: google.maps.places.PlaceResult[], category: string) => {
552
if (category === 'all') {
553
setFilteredPlaces(placesToFilter);
554
} else {
555
const filtered = placesToFilter.filter(place =>
556
place.types?.includes(category as any)
557
);
558
setFilteredPlaces(filtered);
559
}
560
};
561
562
React.useEffect(() => {
563
filterPlaces(places, selectedCategory);
564
}, [selectedCategory, places]);
565
566
const getMarkerColor = (place: google.maps.places.PlaceResult) => {
567
const types = place.types || [];
568
if (types.includes('restaurant' as any)) return 'red';
569
if (types.includes('lodging' as any)) return 'blue';
570
if (types.includes('tourist_attraction' as any)) return 'green';
571
if (types.includes('store' as any)) return 'orange';
572
return 'gray';
573
};
574
575
return (
576
<LoadScript googleMapsApiKey="YOUR_API_KEY" libraries={['places']}>
577
<div>
578
<div style={{ padding: '10px', background: '#f0f0f0' }}>
579
<div style={{ marginBottom: '10px' }}>
580
<StandaloneSearchBox
581
onLoad={(searchBox) => { searchBoxRef.current = searchBox; }}
582
onPlacesChanged={onPlacesChanged}
583
bounds={mapBounds || undefined}
584
>
585
<input
586
type="text"
587
placeholder="Search for restaurants, hotels, attractions..."
588
style={{
589
width: '100%',
590
height: '40px',
591
padding: '0 15px',
592
fontSize: '16px',
593
border: '1px solid #ccc',
594
borderRadius: '4px'
595
}}
596
/>
597
</StandaloneSearchBox>
598
</div>
599
600
<div style={{ marginBottom: '10px' }}>
601
<label>Filter by category: </label>
602
<select
603
value={selectedCategory}
604
onChange={(e) => setSelectedCategory(e.target.value)}
605
style={{ marginLeft: '10px', padding: '5px' }}
606
>
607
{categories.map(category => (
608
<option key={category.value} value={category.value}>
609
{category.label}
610
</option>
611
))}
612
</select>
613
</div>
614
615
{filteredPlaces.length > 0 && (
616
<div>
617
<div>Showing {filteredPlaces.length} of {places.length} places</div>
618
</div>
619
)}
620
</div>
621
622
{filteredPlaces.length > 0 && (
623
<div style={{
624
height: '150px',
625
overflowY: 'auto',
626
padding: '10px',
627
background: 'white',
628
borderTop: '1px solid #ccc'
629
}}>
630
{filteredPlaces.map((place, index) => (
631
<div
632
key={index}
633
style={{
634
padding: '8px',
635
borderBottom: '1px solid #eee',
636
cursor: 'pointer',
637
display: 'flex',
638
alignItems: 'center'
639
}}
640
onClick={() => {
641
if (place.geometry && place.geometry.location && mapRef.current) {
642
mapRef.current.setCenter(place.geometry.location);
643
mapRef.current.setZoom(16);
644
}
645
}}
646
>
647
<div
648
style={{
649
width: '12px',
650
height: '12px',
651
borderRadius: '50%',
652
backgroundColor: getMarkerColor(place),
653
marginRight: '10px',
654
flexShrink: 0
655
}}
656
/>
657
<div>
658
<div><strong>{place.name}</strong></div>
659
<div style={{ fontSize: '12px', color: '#666' }}>
660
{place.formatted_address}
661
</div>
662
{place.rating && (
663
<div style={{ fontSize: '12px', color: '#666' }}>
664
Rating: {place.rating} ⭐
665
</div>
666
)}
667
</div>
668
</div>
669
))}
670
</div>
671
)}
672
673
<GoogleMap
674
center={{ lat: 40.7128, lng: -74.0060 }}
675
zoom={13}
676
mapContainerStyle={{ width: '100%', height: '400px' }}
677
onLoad={(map) => {
678
mapRef.current = map;
679
}}
680
onBoundsChanged={() => {
681
if (mapRef.current) {
682
const bounds = mapRef.current.getBounds();
683
if (bounds) {
684
setMapBounds(bounds);
685
}
686
}
687
}}
688
>
689
{filteredPlaces.map((place, index) =>
690
place.geometry && place.geometry.location && (
691
<Marker
692
key={index}
693
position={place.geometry.location.toJSON()}
694
title={place.name}
695
icon={`https://maps.google.com/mapfiles/ms/icons/${getMarkerColor(place)}-dot.png`}
696
/>
697
)
698
)}
699
</GoogleMap>
700
</div>
701
</LoadScript>
702
);
703
}
704
705
// Search box with custom UI and place details
706
function CustomSearchInterface() {
707
const [places, setPlaces] = useState<google.maps.places.PlaceResult[]>([]);
708
const [selectedPlace, setSelectedPlace] = useState<google.maps.places.PlaceResult | null>(null);
709
const [searchValue, setSearchValue] = useState('');
710
const searchBoxRef = useRef<google.maps.places.SearchBox | null>(null);
711
712
const performSearch = () => {
713
if (searchBoxRef.current && searchValue.trim()) {
714
// Trigger search by setting the input value and firing the event
715
const input = document.getElementById('search-input') as HTMLInputElement;
716
if (input) {
717
input.value = searchValue;
718
google.maps.event.trigger(input, 'focus');
719
google.maps.event.trigger(input, 'keydown', { keyCode: 13 });
720
}
721
}
722
};
723
724
const onPlacesChanged = () => {
725
if (searchBoxRef.current) {
726
const results = searchBoxRef.current.getPlaces();
727
if (results && results.length > 0) {
728
setPlaces(results);
729
setSelectedPlace(results[0]); // Select first result by default
730
}
731
}
732
};
733
734
return (
735
<LoadScript googleMapsApiKey="YOUR_API_KEY" libraries={['places']}>
736
<div style={{ display: 'flex', height: '500px' }}>
737
{/* Search Panel */}
738
<div style={{ width: '350px', padding: '15px', background: '#f8f9fa', borderRight: '1px solid #dee2e6' }}>
739
<h3 style={{ margin: '0 0 15px 0' }}>Place Search</h3>
740
741
<div style={{ marginBottom: '15px' }}>
742
<StandaloneSearchBox
743
onLoad={(searchBox) => { searchBoxRef.current = searchBox; }}
744
onPlacesChanged={onPlacesChanged}
745
>
746
<input
747
id="search-input"
748
type="text"
749
value={searchValue}
750
onChange={(e) => setSearchValue(e.target.value)}
751
onKeyPress={(e) => e.key === 'Enter' && performSearch()}
752
placeholder="Search places..."
753
style={{
754
width: '100%',
755
height: '40px',
756
padding: '0 15px',
757
border: '1px solid #ced4da',
758
borderRadius: '4px',
759
fontSize: '14px'
760
}}
761
/>
762
</StandaloneSearchBox>
763
764
<button
765
onClick={performSearch}
766
style={{
767
width: '100%',
768
marginTop: '10px',
769
padding: '10px',
770
background: '#007bff',
771
color: 'white',
772
border: 'none',
773
borderRadius: '4px',
774
cursor: 'pointer'
775
}}
776
>
777
Search
778
</button>
779
</div>
780
781
{/* Results List */}
782
<div style={{ maxHeight: '300px', overflowY: 'auto' }}>
783
{places.map((place, index) => (
784
<div
785
key={index}
786
onClick={() => setSelectedPlace(place)}
787
style={{
788
padding: '10px',
789
margin: '5px 0',
790
background: selectedPlace === place ? '#e3f2fd' : 'white',
791
border: '1px solid #dee2e6',
792
borderRadius: '4px',
793
cursor: 'pointer'
794
}}
795
>
796
<div style={{ fontWeight: 'bold', marginBottom: '5px' }}>
797
{place.name}
798
</div>
799
<div style={{ fontSize: '12px', color: '#666' }}>
800
{place.formatted_address}
801
</div>
802
{place.rating && (
803
<div style={{ fontSize: '12px', marginTop: '5px' }}>
804
⭐ {place.rating} {place.user_ratings_total && `(${place.user_ratings_total} reviews)`}
805
</div>
806
)}
807
</div>
808
))}
809
</div>
810
811
{/* Selected Place Details */}
812
{selectedPlace && (
813
<div style={{
814
marginTop: '15px',
815
padding: '15px',
816
background: 'white',
817
border: '1px solid #dee2e6',
818
borderRadius: '4px'
819
}}>
820
<h4 style={{ margin: '0 0 10px 0' }}>Place Details</h4>
821
<div><strong>Name:</strong> {selectedPlace.name}</div>
822
<div><strong>Address:</strong> {selectedPlace.formatted_address}</div>
823
<div><strong>Types:</strong> {selectedPlace.types?.join(', ')}</div>
824
{selectedPlace.rating && (
825
<div><strong>Rating:</strong> {selectedPlace.rating} / 5</div>
826
)}
827
{selectedPlace.price_level !== undefined && (
828
<div><strong>Price:</strong> {'$'.repeat(selectedPlace.price_level + 1)}</div>
829
)}
830
</div>
831
)}
832
</div>
833
834
{/* Map */}
835
<div style={{ flex: 1 }}>
836
<GoogleMap
837
center={
838
selectedPlace?.geometry?.location
839
? selectedPlace.geometry.location.toJSON()
840
: { lat: 40.7128, lng: -74.0060 }
841
}
842
zoom={selectedPlace ? 16 : 12}
843
mapContainerStyle={{ width: '100%', height: '100%' }}
844
>
845
{places.map((place, index) =>
846
place.geometry && place.geometry.location && (
847
<Marker
848
key={index}
849
position={place.geometry.location.toJSON()}
850
title={place.name}
851
icon={{
852
url: selectedPlace === place
853
? 'https://maps.google.com/mapfiles/ms/icons/red-dot.png'
854
: 'https://maps.google.com/mapfiles/ms/icons/blue-dot.png',
855
scaledSize: new google.maps.Size(32, 32)
856
}}
857
onClick={() => setSelectedPlace(place)}
858
/>
859
)
860
)}
861
</GoogleMap>
862
</div>
863
</div>
864
</LoadScript>
865
);
866
}
867
```
868
869
### Places API Integration Best Practices
870
871
Guidelines for effective Places API usage and optimization strategies.
872
873
```typescript { .api }
874
/**
875
* Places API integration best practices and optimization
876
*/
877
interface PlacesAPIBestPractices {
878
// Field selection optimization
879
requestOnlyNeededFields: boolean; // Reduce API costs by requesting specific fields
880
essentialFields: string[]; // Minimal required fields for basic functionality
881
extendedFields: string[]; // Additional fields for enhanced features
882
883
// Geographic optimization
884
useBounds: boolean; // Restrict searches to relevant geographic areas
885
useStrictBounds: boolean; // Enforce strict boundary compliance
886
887
// Performance optimization
888
implementDebouncing: boolean; // Debounce user input to reduce API calls
889
cacheResults: boolean; // Cache place results for repeated queries
890
useSessionTokens: boolean; // Group related requests for cost optimization
891
}
892
893
// Example field optimization
894
const PLACE_FIELDS = {
895
basic: ['place_id', 'name', 'formatted_address', 'geometry'],
896
contact: ['international_phone_number', 'website', 'opening_hours'],
897
atmosphere: ['rating', 'user_ratings_total', 'price_level', 'reviews'],
898
media: ['photos', 'icon', 'icon_background_color'],
899
detailed: ['types', 'vicinity', 'plus_code', 'url']
900
};
901
902
// Example debounced autocomplete implementation
903
const useDebouncedAutocomplete = (delay: number = 300) => {
904
const [searchTerm, setSearchTerm] = React.useState('');
905
const [debouncedTerm, setDebouncedTerm] = React.useState('');
906
907
React.useEffect(() => {
908
const timer = setTimeout(() => {
909
setDebouncedTerm(searchTerm);
910
}, delay);
911
912
return () => clearTimeout(timer);
913
}, [searchTerm, delay]);
914
915
return { searchTerm, setSearchTerm, debouncedTerm };
916
};
917
```