<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Blog - Bill.IHCha]]></title><description><![CDATA[Blog - Bill.IHCha]]></description><link>https://blog.bill-zhanxg.com</link><generator>RSS for Node</generator><lastBuildDate>Wed, 06 May 2026 12:55:33 GMT</lastBuildDate><atom:link href="https://blog.bill-zhanxg.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Google Map Places Autocomplete with Next.js]]></title><description><![CDATA[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 f...]]></description><link>https://blog.bill-zhanxg.com/google-map-places-autocomplete-with-nextjs</link><guid isPermaLink="true">https://blog.bill-zhanxg.com/google-map-places-autocomplete-with-nextjs</guid><category><![CDATA[Next.js]]></category><category><![CDATA[Google Map API]]></category><dc:creator><![CDATA[Bill Zhang]]></dc:creator><pubDate>Wed, 16 Apr 2025 07:04:58 GMT</pubDate><content:encoded><![CDATA[<p>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.</p>
<p>I have made my own custom react hook for checking whether Google Map API script needs to be loaded or not.</p>
<p><code>./hooks/use-google-map-api.tsx</code></p>
<pre><code class="lang-typescript"><span class="hljs-string">'use client'</span>;

<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> React <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;

<span class="hljs-keyword">interface</span> State {
    needGoogleMapApi: <span class="hljs-built_in">boolean</span>;
    isReady: <span class="hljs-built_in">boolean</span>;
}

<span class="hljs-keyword">const</span> listeners: <span class="hljs-built_in">Array</span>&lt;<span class="hljs-function">(<span class="hljs-params">state: State</span>) =&gt;</span> <span class="hljs-built_in">void</span>&gt; = [];
<span class="hljs-keyword">let</span> memoryState: State = { needGoogleMapApi: <span class="hljs-literal">false</span>, isReady: <span class="hljs-literal">false</span> };
<span class="hljs-keyword">let</span> firstTime = <span class="hljs-literal">true</span>;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">dispatch</span>(<span class="hljs-params">updates: Partial&lt;State&gt;</span>) </span>{
    memoryState = { ...memoryState, ...updates };
    listeners.forEach(<span class="hljs-function">(<span class="hljs-params">listener</span>) =&gt;</span> {
        listener(memoryState);
    });
}

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">useGoogleMapApi</span>(<span class="hljs-params">autoLoad: <span class="hljs-built_in">boolean</span> = <span class="hljs-literal">false</span></span>) </span>{
    <span class="hljs-keyword">const</span> [state, setState] = React.useState&lt;State&gt;(<span class="hljs-function">() =&gt;</span> {
        <span class="hljs-comment">// Synchronously set needGoogleMapApi if autoLoad is true (genius)</span>
        <span class="hljs-keyword">if</span> (autoLoad &amp;&amp; !memoryState.needGoogleMapApi) {
            memoryState.needGoogleMapApi = <span class="hljs-literal">true</span>;
        }
        <span class="hljs-keyword">return</span> memoryState;
    });

    React.useEffect(<span class="hljs-function">() =&gt;</span> {
        listeners.push(setState);
        <span class="hljs-keyword">if</span> ((autoLoad &amp;&amp; !memoryState.needGoogleMapApi) || firstTime) {
            dispatch({ needGoogleMapApi: <span class="hljs-literal">true</span> });
            firstTime = <span class="hljs-literal">false</span>;
        }
        <span class="hljs-keyword">return</span> <span class="hljs-function">() =&gt;</span> {
            <span class="hljs-keyword">const</span> index = listeners.indexOf(setState);
            <span class="hljs-keyword">if</span> (index &gt; <span class="hljs-number">-1</span>) {
                listeners.splice(index, <span class="hljs-number">1</span>);
            }
        };
    }, [autoLoad]);

    <span class="hljs-keyword">const</span> loadGoogleMap = React.useCallback(<span class="hljs-function">() =&gt;</span> {
        <span class="hljs-keyword">if</span> (!memoryState.needGoogleMapApi) {
            dispatch({ needGoogleMapApi: <span class="hljs-literal">true</span> });
        }
        <span class="hljs-keyword">return</span> state;
    }, [state]);

    <span class="hljs-keyword">const</span> onReady = React.useCallback(<span class="hljs-function">() =&gt;</span> {
        dispatch({ isReady: <span class="hljs-literal">true</span> });
    }, []);

    <span class="hljs-keyword">return</span> {
        ...state,
        loadGoogleMap,
        onReady,
    };
}
</code></pre>
<p>The component that is responsible for loading the Google Script tag</p>
<p><code>./components/google-map-api.tsx</code></p>
<pre><code class="lang-typescript"><span class="hljs-string">'use client'</span>;

<span class="hljs-keyword">import</span> { useGoogleMapApi } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/hooks/use-google-map-api'</span>;
<span class="hljs-keyword">import</span> Script <span class="hljs-keyword">from</span> <span class="hljs-string">'next/script'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">GoogleMapApi</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">const</span> { needGoogleMapApi, onReady } = useGoogleMapApi();

    <span class="hljs-keyword">if</span> (!needGoogleMapApi) <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;

    <span class="hljs-comment">// Load Google Places API</span>
    <span class="hljs-keyword">return</span> (
        &lt;Script
            defer
            src={<span class="hljs-string">`https://maps.googleapis.com/maps/api/js?key=<span class="hljs-subst">${process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY}</span>&amp;libraries=places`</span>}
            onReady={onReady}
        /&gt;
    );
}
</code></pre>
<p>You just need to load this file in the <code>layout.tsx</code> and you’re good to go:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { GoogleMapApi } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/components/google-map-api'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">RootLayout</span>(<span class="hljs-params">{ children }: { children: React.ReactNode }</span>) </span>{
    <span class="hljs-keyword">return</span> (
        &lt;html lang=<span class="hljs-string">"en"</span>&gt;
            &lt;body&gt;
                {children}
                &lt;GoogleMapApi /&gt;
            &lt;/body&gt;
        &lt;/html&gt;
    );
}
</code></pre>
<p>Now if you just add the following code to a page that you want to use the API:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> { isReady } = useGoogleMapApi(<span class="hljs-literal">true</span>);
</code></pre>
<p>Or you could lazily load it by doing so:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> { isReady, loadGoogleMap } = useGoogleMapApi(<span class="hljs-literal">true</span>);
useEffect(<span class="hljs-function">() =&gt;</span> {
    loadGoogleMap();
}, [])
</code></pre>
<p>Here is an example of my app and my UI, we’ll be using the package <code>use-places-autocomplete</code> for easy integration (also assume DasiyUI v4 and Tailwindcss v3 is installed):</p>
<pre><code class="lang-bash">npm i use-places-autocomplete
</code></pre>
<p><code>LocationInput.tsx</code></p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { useGoogleMapApi } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/hooks/use-google-map-api'</span>;
<span class="hljs-keyword">import</span> classNames <span class="hljs-keyword">from</span> <span class="hljs-string">'classnames'</span>;
<span class="hljs-keyword">import</span> { memo, useEffect, useId, useRef, useState } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;
<span class="hljs-keyword">import</span> usePlacesAutocomplete <span class="hljs-keyword">from</span> <span class="hljs-string">'use-places-autocomplete'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> LocationInput = memo(<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">LocationInput</span>(<span class="hljs-params">{
    defaultValue,
    onChange,
    disabled,
    className,
    ...props
}: {
    defaultValue: <span class="hljs-built_in">string</span>;
    onChange: (value: <span class="hljs-built_in">string</span>) =&gt; <span class="hljs-built_in">void</span>;
    disabled?: <span class="hljs-built_in">boolean</span>;
    className?: <span class="hljs-built_in">string</span>;
} &amp; Omit&lt;
    React.InputHTMLAttributes&lt;HTMLInputElement&gt;,
    'onChange' | 'value' | 'defaultValue' | 'disabled' | 'className'
&gt;</span>) </span>{
    <span class="hljs-keyword">const</span> inputRef = useRef&lt;HTMLInputElement&gt;(<span class="hljs-literal">null</span>);
    <span class="hljs-keyword">const</span> { isReady } = useGoogleMapApi(<span class="hljs-literal">true</span>);
    <span class="hljs-keyword">const</span> [trackedValue, setTrackedValue] = useState(defaultValue);
    <span class="hljs-keyword">const</span> [selectedIndex, setSelectedIndex] = useState(<span class="hljs-number">-1</span>);
    <span class="hljs-keyword">const</span> uniqueId = useId();
    <span class="hljs-keyword">const</span> listId = <span class="hljs-string">`location-list-<span class="hljs-subst">${uniqueId}</span>`</span>;

    <span class="hljs-keyword">const</span> {
        ready,
        value,
        suggestions: { status, data },
        setValue,
        clearSuggestions,
        init,
    } = usePlacesAutocomplete({
        debounce: <span class="hljs-number">100</span>,
        initOnMount: <span class="hljs-literal">false</span>,
    });

    useEffect(<span class="hljs-function">() =&gt;</span> {
        <span class="hljs-keyword">if</span> (isReady) init();
    }, [isReady, init]);

    <span class="hljs-comment">// Custom validation for the input</span>
    useEffect(<span class="hljs-function">() =&gt;</span> {
        <span class="hljs-keyword">if</span> (trackedValue) inputRef.current?.setCustomValidity(<span class="hljs-string">''</span>);
        <span class="hljs-keyword">else</span> inputRef.current?.setCustomValidity(<span class="hljs-string">'You Must Select a Location'</span>);
    }, [trackedValue]);

    useEffect(<span class="hljs-function">() =&gt;</span> {
        onChange(trackedValue);
        <span class="hljs-comment">// eslint-disable-next-line react-compiler/react-compiler</span>
        <span class="hljs-comment">// eslint-disable-next-line react-hooks/exhaustive-deps</span>
    }, [trackedValue]);

    <span class="hljs-keyword">const</span> handleKeyDown = <span class="hljs-function">(<span class="hljs-params">e: React.KeyboardEvent&lt;HTMLInputElement&gt;</span>) =&gt;</span> {
        <span class="hljs-keyword">if</span> (status !== <span class="hljs-string">'OK'</span>) <span class="hljs-keyword">return</span>;

        <span class="hljs-keyword">switch</span> (e.key) {
            <span class="hljs-keyword">case</span> <span class="hljs-string">'ArrowDown'</span>:
                e.preventDefault();
                setSelectedIndex(<span class="hljs-function">(<span class="hljs-params">prev</span>) =&gt;</span> <span class="hljs-built_in">Math</span>.min(prev + <span class="hljs-number">1</span>, data.length - <span class="hljs-number">1</span>));
                <span class="hljs-keyword">break</span>;
            <span class="hljs-keyword">case</span> <span class="hljs-string">'ArrowUp'</span>:
                e.preventDefault();
                setSelectedIndex(<span class="hljs-function">(<span class="hljs-params">prev</span>) =&gt;</span> <span class="hljs-built_in">Math</span>.max(prev - <span class="hljs-number">1</span>, <span class="hljs-number">-1</span>));
                <span class="hljs-keyword">break</span>;
            <span class="hljs-keyword">case</span> <span class="hljs-string">'Enter'</span>:
                e.preventDefault();
                <span class="hljs-keyword">if</span> (selectedIndex &gt;= <span class="hljs-number">0</span>) {
                    <span class="hljs-keyword">const</span> suggestion = data[selectedIndex];
                    setTrackedValue(suggestion.description);
                    setValue(suggestion.structured_formatting.main_text, <span class="hljs-literal">false</span>);
                    clearSuggestions();
                    setSelectedIndex(<span class="hljs-number">-1</span>);
                }
                <span class="hljs-keyword">break</span>;
            <span class="hljs-keyword">case</span> <span class="hljs-string">'Escape'</span>:
                clearSuggestions();
                setSelectedIndex(<span class="hljs-number">-1</span>);
                <span class="hljs-keyword">break</span>;
        }
    };

    <span class="hljs-keyword">return</span> (
        &lt;div className={classNames(<span class="hljs-string">'dropdown'</span>, className)}&gt;
            &lt;input
                ref={inputRef}
                <span class="hljs-keyword">type</span>=<span class="hljs-string">"text"</span>
                className=<span class="hljs-string">"!input !input-bordered w-full invalid:input-error"</span>
                value={!ready ? <span class="hljs-string">'Loading...'</span> : value || trackedValue}
                onChange={<span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> {
                    setValue(e.target.value);
                    setTrackedValue(<span class="hljs-string">''</span>);
                    setSelectedIndex(<span class="hljs-number">-1</span>);
                }}
                onKeyDown={handleKeyDown}
                disabled={!ready || disabled}
                placeholder={<span class="hljs-string">'Enter a location'</span>}
                role=<span class="hljs-string">"combobox"</span>
                aria-expanded={status === <span class="hljs-string">'OK'</span>}
                aria-controls={listId}
                aria-label=<span class="hljs-string">"Location search"</span>
                aria-autocomplete=<span class="hljs-string">"list"</span>
                {...props}
            /&gt;
            {<span class="hljs-comment">/* We can use the "status" to decide whether we should display the dropdown or not */</span>}
            {status === <span class="hljs-string">'OK'</span> &amp;&amp; (
                &lt;ul
                    id={listId}
                    role=<span class="hljs-string">"listbox"</span>
                    className=<span class="hljs-string">"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"</span>
                &gt;
                    {data.map(<span class="hljs-function">(<span class="hljs-params">suggestion, index</span>) =&gt;</span> {
                        <span class="hljs-keyword">const</span> {
                            place_id,
                            description,
                            structured_formatting: { main_text, secondary_text },
                        } = suggestion;

                        <span class="hljs-keyword">return</span> (
                            &lt;li
                                key={place_id}
                                role=<span class="hljs-string">"option"</span>
                                aria-selected={index === selectedIndex}
                                className={classNames(index === selectedIndex &amp;&amp; <span class="hljs-string">'bg-base-200'</span>)}
                                <span class="hljs-comment">// Prevent the menu from disappearing when clicking on the suggestion</span>
                                onMouseDown={<span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> e.preventDefault()}
                                onClick={<span class="hljs-function">() =&gt;</span> {
                                    setTrackedValue(description);
                                    <span class="hljs-comment">// When the user selects a place, we can replace the keyword without request data from API</span>
                                    <span class="hljs-comment">// by setting the second parameter to "false"</span>
                                    setValue(main_text, <span class="hljs-literal">false</span>);
                                    clearSuggestions();
                                    setSelectedIndex(<span class="hljs-number">-1</span>);
                                }}
                            &gt;
                                &lt;p className=<span class="hljs-string">"flex flex-col items-start gap-0"</span>&gt;
                                    &lt;strong&gt;{main_text}&lt;<span class="hljs-regexp">/strong&gt; &lt;small&gt;{secondary_text}&lt;/</span>small&gt;
                                &lt;/p&gt;
                            &lt;/li&gt;
                        );
                    })}
                &lt;/ul&gt;
            )}
        &lt;/div&gt;
    );
});
</code></pre>
]]></content:encoded></item></channel></rss>