# Google Map Places Autocomplete with Next.js

Google APIs documents are annoying to read, and they lack support for popular frameworks such as React and Next.js. In this article I’ll show you how I implemented lazy loading the API the simplest way possible.

I have made my own custom react hook for checking whether Google Map API script needs to be loaded or not.

`./hooks/use-google-map-api.tsx`

```typescript
'use client';

import * as React from 'react';

interface State {
	needGoogleMapApi: boolean;
	isReady: boolean;
}

const listeners: Array<(state: State) => void> = [];
let memoryState: State = { needGoogleMapApi: false, isReady: false };
let firstTime = true;

function dispatch(updates: Partial<State>) {
	memoryState = { ...memoryState, ...updates };
	listeners.forEach((listener) => {
		listener(memoryState);
	});
}

export function useGoogleMapApi(autoLoad: boolean = false) {
	const [state, setState] = React.useState<State>(() => {
		// Synchronously set needGoogleMapApi if autoLoad is true (genius)
		if (autoLoad && !memoryState.needGoogleMapApi) {
			memoryState.needGoogleMapApi = true;
		}
		return memoryState;
	});

	React.useEffect(() => {
		listeners.push(setState);
		if ((autoLoad && !memoryState.needGoogleMapApi) || firstTime) {
			dispatch({ needGoogleMapApi: true });
			firstTime = false;
		}
		return () => {
			const index = listeners.indexOf(setState);
			if (index > -1) {
				listeners.splice(index, 1);
			}
		};
	}, [autoLoad]);

	const loadGoogleMap = React.useCallback(() => {
		if (!memoryState.needGoogleMapApi) {
			dispatch({ needGoogleMapApi: true });
		}
		return state;
	}, [state]);

	const onReady = React.useCallback(() => {
		dispatch({ isReady: true });
	}, []);

	return {
		...state,
		loadGoogleMap,
		onReady,
	};
}
```

The component that is responsible for loading the Google Script tag

`./components/google-map-api.tsx`

```typescript
'use client';

import { useGoogleMapApi } from '@/hooks/use-google-map-api';
import Script from 'next/script';

export function GoogleMapApi() {
	const { needGoogleMapApi, onReady } = useGoogleMapApi();

	if (!needGoogleMapApi) return null;

	// Load Google Places API
	return (
		<Script
			defer
			src={`https://maps.googleapis.com/maps/api/js?key=${process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY}&libraries=places`}
			onReady={onReady}
		/>
	);
}
```

You just need to load this file in the `layout.tsx` and you’re good to go:

```typescript
import { GoogleMapApi } from '@/components/google-map-api';

export default async function RootLayout({ children }: { children: React.ReactNode }) {
	return (
		<html lang="en">
			<body>
				{children}
				<GoogleMapApi />
			</body>
		</html>
	);
}
```

Now if you just add the following code to a page that you want to use the API:

```typescript
const { isReady } = useGoogleMapApi(true);
```

Or you could lazily load it by doing so:

```typescript
const { isReady, loadGoogleMap } = useGoogleMapApi(true);
useEffect(() => {
    loadGoogleMap();
}, [])
```

Here is an example of my app and my UI, we’ll be using the package `use-places-autocomplete` for easy integration (also assume DasiyUI v4 and Tailwindcss v3 is installed):

```bash
npm i use-places-autocomplete
```

`LocationInput.tsx`

```typescript
import { useGoogleMapApi } from '@/hooks/use-google-map-api';
import classNames from 'classnames';
import { memo, useEffect, useId, useRef, useState } from 'react';
import usePlacesAutocomplete from 'use-places-autocomplete';

export const LocationInput = memo(function LocationInput({
	defaultValue,
	onChange,
	disabled,
	className,
	...props
}: {
	defaultValue: string;
	onChange: (value: string) => void;
	disabled?: boolean;
	className?: string;
} & Omit<
	React.InputHTMLAttributes<HTMLInputElement>,
	'onChange' | 'value' | 'defaultValue' | 'disabled' | 'className'
>) {
	const inputRef = useRef<HTMLInputElement>(null);
	const { isReady } = useGoogleMapApi(true);
	const [trackedValue, setTrackedValue] = useState(defaultValue);
	const [selectedIndex, setSelectedIndex] = useState(-1);
	const uniqueId = useId();
	const listId = `location-list-${uniqueId}`;

	const {
		ready,
		value,
		suggestions: { status, data },
		setValue,
		clearSuggestions,
		init,
	} = usePlacesAutocomplete({
		debounce: 100,
		initOnMount: false,
	});

	useEffect(() => {
		if (isReady) init();
	}, [isReady, init]);

	// Custom validation for the input
	useEffect(() => {
		if (trackedValue) inputRef.current?.setCustomValidity('');
		else inputRef.current?.setCustomValidity('You Must Select a Location');
	}, [trackedValue]);

	useEffect(() => {
		onChange(trackedValue);
		// eslint-disable-next-line react-compiler/react-compiler
		// eslint-disable-next-line react-hooks/exhaustive-deps
	}, [trackedValue]);

	const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
		if (status !== 'OK') return;

		switch (e.key) {
			case 'ArrowDown':
				e.preventDefault();
				setSelectedIndex((prev) => Math.min(prev + 1, data.length - 1));
				break;
			case 'ArrowUp':
				e.preventDefault();
				setSelectedIndex((prev) => Math.max(prev - 1, -1));
				break;
			case 'Enter':
				e.preventDefault();
				if (selectedIndex >= 0) {
					const suggestion = data[selectedIndex];
					setTrackedValue(suggestion.description);
					setValue(suggestion.structured_formatting.main_text, false);
					clearSuggestions();
					setSelectedIndex(-1);
				}
				break;
			case 'Escape':
				clearSuggestions();
				setSelectedIndex(-1);
				break;
		}
	};

	return (
		<div className={classNames('dropdown', className)}>
			<input
				ref={inputRef}
				type="text"
				className="!input !input-bordered w-full invalid:input-error"
				value={!ready ? 'Loading...' : value || trackedValue}
				onChange={(e) => {
					setValue(e.target.value);
					setTrackedValue('');
					setSelectedIndex(-1);
				}}
				onKeyDown={handleKeyDown}
				disabled={!ready || disabled}
				placeholder={'Enter a location'}
				role="combobox"
				aria-expanded={status === 'OK'}
				aria-controls={listId}
				aria-label="Location search"
				aria-autocomplete="list"
				{...props}
			/>
			{/* We can use the "status" to decide whether we should display the dropdown or not */}
			{status === 'OK' && (
				<ul
					id={listId}
					role="listbox"
					className="menu dropdown-content z-50 w-full overflow-x-auto rounded-box bg-base-100 p-2 shadow-2xl lg:w-screen lg:max-w-96"
				>
					{data.map((suggestion, index) => {
						const {
							place_id,
							description,
							structured_formatting: { main_text, secondary_text },
						} = suggestion;

						return (
							<li
								key={place_id}
								role="option"
								aria-selected={index === selectedIndex}
								className={classNames(index === selectedIndex && 'bg-base-200')}
								// Prevent the menu from disappearing when clicking on the suggestion
								onMouseDown={(e) => e.preventDefault()}
								onClick={() => {
									setTrackedValue(description);
									// When the user selects a place, we can replace the keyword without request data from API
									// by setting the second parameter to "false"
									setValue(main_text, false);
									clearSuggestions();
									setSelectedIndex(-1);
								}}
							>
								<p className="flex flex-col items-start gap-0">
									<strong>{main_text}</strong> <small>{secondary_text}</small>
								</p>
							</li>
						);
					})}
				</ul>
			)}
		</div>
	);
});
```
