How do I install GA4 correctly in a React, Next.js, Nuxt, or Vue single-page app?
Quick answer
Drop the gtag.js snippet in once, at the app shell level (root layout, _app, or main.js) - never inside a page or route component, or it reloads on every navigation. Disable GA4's automatic page_view on load, then fire a manual page_view (or an SPA-aware config call) on every client-side route change, because a framework router never triggers a real browser page load for GA4 to detect on its own.
Why This Happens
GA4's default snippet assumes the browser fires a full page load once, which is true for a classic multi-page site and false for every SPA framework. React Router, Next.js's client router, Nuxt's Vue Router, and Vue Router all swap content in place without a network navigation, so the gtag.js library never sees a reason to log a second page_view after the first one.
The fix isn't framework-specific magic - it's the same three-part pattern regardless of stack: load the tag once, stop it from assuming every render is a new page, and tell it explicitly when the route actually changes. Most installation mistakes come from treating GA4 like a static-site snippet and pasting it into a component that itself gets mounted and unmounted on navigation, which produces either zero page_view events after the first load or a flood of duplicate ones.
Fix It
Load the snippet once, at the root
Place the gtag.js <script> tags in the single highest-level entry point your framework offers: the root layout (Next.js App Router app/layout.tsx), pages/_app.tsx (Pages Router), nuxt.config.ts via a plugin, or main.js for a plain Vue/React SPA. It should mount exactly once for the life of the tab.
Send the initial config with page_view disabled
In the inline config call, add send_page_view: false to the config parameters: gtag('config', 'G-XXXXXXX', { send_page_view: false }). This stops GA4 from auto-logging a page_view for the very first load too, so your manual event is the only source of truth from the start.
Fire a manual page_view on every route change
Hook into your router's navigation lifecycle - Next.js's usePathname/useSearchParams combo with a useEffect, Vue Router's router.afterEach, or React Router's useLocation - and call gtag('event', 'page_view', { page_path: newPath, page_title: document.title, page_location: window.location.href }) each time the path changes.
Guard against firing on the very first render twice
If your router hook also runs on initial mount, you'll double-log the first page. Skip the first invocation with a ref/flag (e.g. a useRef(true) in React that flips to false after the first effect run) so the very first view is only logged once.
For Next.js specifically, prefer the official Script component
Load gtag.js with <Script strategy="afterInteractive"> from next/script (or @next/third-parties/google's GoogleAnalytics component) rather than a raw <script> tag, so it respects Next's hydration timing instead of racing it.
How To Verify It Worked
Open GA4 Admin > DebugView (or install the Google Analytics Debugger extension / append ?gtm_debug=1/enable debug mode) and click through three or four routes in your app without reloading the tab. You should see exactly one page_view event per route change, each carrying the correct page_location for that route.
A failing result looks like one of two patterns: no new page_view events at all after the first load (the manual firing hook isn't wired up), or two identical page_view events per navigation (the default auto page_view was never disabled, or the router hook fires twice). Fix whichever pattern you see using the steps above, then re-test the same three routes.
Still Not Fixed?
We audit and fix GA4 implementations for a living - if this doesn't resolve it, the next step is usually a full event-by-event audit.
Talk to a GA4 Consultant