Next.js 14 Google Analytics gtag function undefined error

I’m having trouble with Google Analytics in my Next.js 14.0.4 project. When my page loads, I get an error saying window.gtag is not a function.

The problem started after upgrading to Next.js 14. I’m using @types/gtag.js and client-only packages. The issue seems to be that gtag isn’t loading properly, which breaks my cookie consent system.

Here’s my main layout structure:

<html lang='en'>
    <head>
        <AnalyticsScript />
    </head>
    <body className='main-container'>
        <NotificationToast />
        <HeaderNav />
        <main className='content-wrapper'>
            {children}
        </main>
        <FooterSection />
        <ConsentModal />
    </body>
</html>

And my consent component:

'use client';

type CookieSettings = {
    advertising: boolean
    tracking: boolean
}

export default function ConsentModal() {
    const [userConsent, setUserConsent] = useState<CookieSettings>();

    useEffect(() => {
        const savedConsent = getStoredValue("user_consent", null);
        setUserConsent(savedConsent);
    }, []);

    useEffect(() => {
        const adConsent = userConsent?.advertising ? 'granted' : 'denied'
        const trackingConsent = userConsent?.tracking ? 'granted' : 'denied'

        if (typeof window !== 'undefined' && typeof window.gtag !== 'undefined') {
            window.gtag("consent", 'update', {
                'analytics_storage': trackingConsent,
                'ad_storage': adConsent,
            });
        } else {
            console.warn("gtag function not available");
        }

        storeValue("user_consent", userConsent);
    }, [userConsent]);

    return (
        <div className={`consent-banner ${userConsent != null ? "hidden" : "flex"}`}>
            <div className='banner-content'>
                <p>We use cookies for analytics</p>
                <div className='button-group'>
                    <Button onClick={() => setUserConsent({ advertising: false, tracking: true })}>
                        Essential Only
                    </Button>
                    <Button onClick={() => setUserConsent({ advertising: true, tracking: true })}>
                        Accept All
                    </Button>
                </div>
            </div>
        </div>
    )
}

Even with the conditional check, gtag never becomes available. How can I make sure Google Analytics loads properly before trying to use it?

Been there, done that. Your AnalyticsScript loads after the client component mounts - that’s the problem.

I fixed this with a gtag context provider that tracks loading state. Here’s what worked:

Wrap your app with a GtagProvider that monitors when the script loads. Add an onLoad callback to your AnalyticsScript that updates the provider state.

Then modify your ConsentModal to use this context:

const { isGtagReady } = useGtag();

useEffect(() => {
    if (!isGtagReady) return;
    
    const adConsent = userConsent?.advertising ? 'granted' : 'denied'
    const trackingConsent = userConsent?.tracking ? 'granted' : 'denied'
    
    window.gtag("consent", 'update', {
        'analytics_storage': trackingConsent,
        'ad_storage': adConsent,
    });
    
    storeValue("user_consent", userConsent);
}, [userConsent, isGtagReady]);

Now your consent logic waits for gtag to actually be ready instead of just checking if it exists at mount time.

Also use strategy="beforeInteractive" in your Script tag. This loads GA before hydration happens.

This timing issue is common with Next.js 14’s hydration process. Your AnalyticsScript component isn’t loading gtag before ConsentModal tries to access it. I encountered the same problem and resolved it with a gtag availability checker. Instead of just checking if gtag exists, I implemented a polling mechanism to wait for it to load: const waitForGtag = (callback: () => void, maxAttempts = 10) => { let attempts = 0; const checkGtag = () => { if (typeof window !== ‘undefined’ && typeof window.gtag === ‘function’) { callback(); } else if (attempts < maxAttempts) { attempts++; setTimeout(checkGtag, 200); } }; checkGtag(); }; In your useEffect, replace the direct gtag call with: waitForGtag(() => { window.gtag(“consent”, ‘update’, { ‘analytics_storage’: trackingConsent, ‘ad_storage’: adConsent, }); }); This approach provides Google Analytics the necessary time to initialize before your consent logic runs.

This is a script loading order issue with Next.js 14’s app router. Your ConsentModal is mounting before Google Analytics finishes loading. I hit the same problem recently and fixed it by tweaking the AnalyticsScript component to handle initialization properly. Make sure your AnalyticsScript includes both the GA library script and gtag config, and use the beforeInteractive strategy in the Script component. You can also build a custom hook that tracks when gtag’s ready using a boolean flag - set it to true once gtag’s available. Then your ConsentModal can subscribe to this state and only call gtag when it’s actually loaded. Your conditional check is smart, but it only runs once during useEffect instead of waiting for gtag to be ready.