React GA4 Best Practices: Integrating Google Analytics 4 with gtag.js in React Applications

Use the Firebase Analytics SDK as a type-safe wrapper around gtag.js to automatically handle script injection, initialization, and event queuing while implementing a custom hook and route tracker for React-specific navigation patterns.

Implementing React GA4 (Google Analytics 4) in modern React applications requires careful handling of client-side routing and script initialization. The firebase/firebase-js-sdk provides a robust Analytics package that wraps the standard gtag.js library, offering TypeScript support and automatic script injection while maintaining full compatibility with GA4's event model.

Organize your React GA4 implementation into three layers to ensure type safety and separation of concerns:


src/
 ├─ firebase/
 │    └─ firebase.ts            // FirebaseApp + Analytics initialization
 ├─ hooks/
 │    └─ useAnalytics.ts        // Returns the Analytics instance
 ├─ components/
 │    └─ PageViewTracker.tsx   // Logs page_view on route changes
 └─ App.tsx                     // Root component

Why this structure works:

  • Dedicated Firebase module: Keeps configuration in one place and guarantees initialization logic in packages/analytics/src/initialize-analytics.ts runs only once. This module handles automatic insertion of the <script src="https://www.googletagmanager.com/gtag/js?id=G-XXXX"> tag if not already present【/cache/repos/github.com/firebase/firebase-js-sdk/main/packages/analytics/src/initialize-analytics.ts#L22-L27】.

  • Custom hook: Provides a type-safe way to access the Analytics instance after the internal initialization promise resolves, preventing "gtag is not a function" errors during the loading phase.

  • Route tracker component: GA4 sends an initial page_view automatically, but for client-side routing (React Router, Next.js, etc.), you must manually fire it on navigation. A component with useEffect listening to the router's location does that reliably.

Step-by-Step React GA4 Implementation

Initialize Firebase and Analytics

Create a configuration module that initializes the Firebase app and wraps the gtag.js initialization:

// src/firebase/firebase.ts
import { initializeApp } from 'firebase/app';
import { getAnalytics, SettingsOptions } from 'firebase/analytics';

const firebaseConfig = {
  apiKey: 'YOUR_API_KEY',
  authDomain: 'YOUR_PROJECT.firebaseapp.com',
  projectId: 'YOUR_PROJECT',
  appId: 'YOUR_APP_ID',
  measurementId: 'G-XXXXXXX', // GA4 measurement ID
};

const app = initializeApp(firebaseConfig);

// Optional: customize gtag/dataLayer names to avoid global conflicts
const analyticsSettings: SettingsOptions = {
  gtagName: 'gtag',
  dataLayerName: 'dataLayer'
};

export const analytics = getAnalytics(app, analyticsSettings);

The getAnalytics call triggers the logic in packages/analytics/src/initialize-analytics.ts, which:

  1. Detects existing gtag scripts via findGtagScriptOnPage【/cache/repos/github.com/firebase/firebase-js-sdk/main/packages/analytics/src/helpers.ts#L33-L45】
  2. Inserts the script tag if missing
  3. Queues the gtag('js', …) and gtag('config', measurementId, …) commands【/cache/repos/github.com/firebase/firebase-js-sdk/main/packages/analytics/src/initialize-analytics.ts#L34-L57】【/cache/repos/github.com/firebase/firebase-js-sdk/main/packages/analytics/src/initialize-analytics.ts#L138-L155】

Create the useAnalytics Hook

Implement a hook that safely exposes the Analytics instance:

// src/hooks/useAnalytics.ts
import { useEffect, useState } from 'react';
import { analytics } from '../firebase/firebase';
import { Analytics } from 'firebase/analytics';

export function useAnalytics(): Analytics | null {
  const [instance, setInstance] = useState<Analytics | null>(null);

  useEffect(() => {
    // The SDK queues calls automatically, so we can set immediately
    setInstance(analytics);
  }, []);

  return instance;
}

Track Page Views in React Router

Create a component that logs page_view events on route changes:

// src/components/PageViewTracker.tsx
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { logEvent } from 'firebase/analytics';
import { useAnalytics } from '../hooks/useAnalytics';

export default function PageViewTracker() {
  const location = useLocation();
  const analytics = useAnalytics();

  useEffect(() => {
    if (!analytics) return;
    
    logEvent(analytics, 'page_view', {
      page_path: location.pathname + location.search,
      page_title: document.title,
    });
  }, [location, analytics]);

  return null;
}

Mount this component inside your router in App.tsx to ensure every navigation triggers a GA4 page view.

Log Custom Events

Use the typed logEvent function for e-commerce or custom interactions:

import { logEvent } from 'firebase/analytics';
import { useAnalytics } from '../hooks/useAnalytics';

function AddToCartButton({ product }) {
  const analytics = useAnalytics();

  const handleAdd = () => {
    if (analytics) {
      logEvent(analytics, 'add_to_cart', {
        items: [{ 
          item_id: product.id, 
          item_name: product.name, 
          price: product.price 
        }],
        currency: 'USD',
      });
    }
  };

  return <button onClick={handleAdd}>Add to cart</button>;
}

The logEvent helper in packages/analytics/src/api.ts wraps the underlying gtag('event', …) call and includes error handling via the SDK's logger【/cache/repos/github.com/firebase/firebase-js-sdk/main/packages/analytics/src/api.ts#L75-L100】.

Advanced React GA4 Best Practices

Practice Implementation Details Source Reference
Custom gtag/dataLayer names Pass gtagName and dataLayerName in SettingsOptions to getAnalytics to avoid global namespace collisions with other tracking tools. public-types.ts defines SettingsOptions【/cache/repos/github.com/firebase/firebase-js-sdk/main/packages/analytics/src/public-types.ts#L23-L28】
Default event parameters Call setDefaultEventParameters immediately after initialization to attach common context (app version, user tier) to every subsequent event. api.ts implements this via the wrapped gtag function【/cache/repos/github.com/firebase/firebase-js-sdk/main/packages/analytics/src/api.ts#L57-L63】
User identification Use setUserId and setUserProperties to enable cross-device reporting and audience segmentation. These map to gtag('config', …) updates. api.ts handles these config updates【/cache/repos/github.com/firebase/firebase-js-sdk/main/packages/analytics/src/api.ts#L89-L108】
Consent management Configure defaultConsentSettingsForInit during initialization for GDPR/CCPA compliance, then update via gtag('consent', 'update', …). initialize-analytics.ts processes default consent【/cache/repos/github.com/firebase/firebase-js-sdk/main/packages/analytics/src/initialize-analytics.ts#L28-L33】
Duplicate script prevention The SDK automatically checks for existing gtag scripts before injection to prevent "gtag is not a function" errors. helpers.ts contains findGtagScriptOnPage【/cache/repos/github.com/firebase/firebase-js-sdk/main/packages/analytics/src/helpers.ts#L33-L45】
Performance optimization Lazy-load the analytics module to reduce initial bundle size. Import firebase/analytics only in components that need it, or use dynamic import().

Summary

  • Use Firebase Analytics as your React GA4 wrapper – it handles gtag.js script injection, initialization, and provides TypeScript types via packages/analytics/src/public-types.ts.
  • Structure your code with a dedicated Firebase config module, a useAnalytics hook for safe instance access, and a PageViewTracker component to handle React Router navigation.
  • Initialize once by calling getAnalytics in your config module; the SDK automatically prevents duplicate script tags using findGtagScriptOnPage in packages/analytics/src/helpers.ts.
  • Track navigation manually because GA4 only sends the initial page_view automatically; use logEvent from packages/analytics/src/api.ts on route changes.
  • Configure advanced features like custom gtag names, default parameters, user properties, and consent settings via the SettingsOptions interface and helper functions defined in the SDK source.

Frequently Asked Questions

How do I prevent duplicate gtag.js script tags when using React GA4 with Firebase?

The Firebase Analytics SDK automatically detects existing gtag scripts before injection. In packages/analytics/src/helpers.ts, the findGtagScriptOnPage function checks for existing <script> tags with the gtag URL, and initializeAnalytics in packages/analytics/src/initialize-analytics.ts only calls insertScriptTag if none are found. This prevents the "gtag is not a function" errors that occur when the library loads twice.

Why do I need to manually track page views in a React single-page application?

GA4 sends an automatic page_view event only on the initial browser page load. In React applications using client-side routing (React Router, Next.js, etc.), subsequent navigation does not trigger a full page reload, so GA4 never receives additional page view events. You must manually call logEvent(analytics, 'page_view', { page_path: location.pathname, page_title: document.title }) inside a component that listens to route changes, as implemented in the PageViewTracker pattern.

How do I set custom gtag or dataLayer names to avoid conflicts with other tracking tools?

Pass a SettingsOptions object as the second argument to getAnalytics in your initialization module. The public-types.ts file defines this interface with gtagName and dataLayerName properties, which the SDK passes to the internal initialization logic in initialize-analytics.ts. This prevents namespace collisions when your page already loads Google Tag Manager or other analytics libraries that define their own global gtag functions.

Configure default consent settings during initialization by including consent parameters in your AnalyticsSettings, which initialize-analytics.ts processes via defaultConsentSettingsForInit before sending the initial gtag('config') command. For dynamic updates (when a user accepts or rejects cookies), call the underlying gtag('consent', 'update', { analytics_storage: 'granted' }) command using the analytics instance obtained from useAnalytics. The SDK's error handling in api.ts ensures these calls fail gracefully if the script hasn't loaded.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →