Google Analytics 4 (GA4) integration in React applications requires careful attention to how the gtag() function is initialized. A seemingly minor implementation detailβ€”using JavaScript's native arguments object versus modern spread operatorsβ€”can mean the difference between working analytics and silent failures.

This guide demonstrates the correct way to implement GA4 in a React + TypeScript application, explaining why certain patterns work and others fail.


The Critical Mistake

❌ Wrong Implementation (Silent Failure)

// This looks modern and clean, but DOES NOT WORK with GA4
export function initGA(): void {
  window.dataLayer = window.dataLayer || [];
  
  function gtag(...args: any[]) {
    window.dataLayer.push(args); // Pushing an array
  }
  
  gtag('js', new Date());
  gtag('config', 'G-XXXXXXXXXX');
}

What Happens

  • βœ… Script loads successfully
  • βœ… window.gtag function is available
  • βœ… Events are added to dataLayer
  • ❌ No tracking beacons are sent to Google Analytics
  • ⚠️ May trigger Google Tag Manager (GTM) mode instead of GA4
  • ⚠️ No console errors or warnings (silent failure)

Why It Fails

The spread operator (...args) creates a true JavaScript Array with these characteristics:

typeof args                    // 'object'
Array.isArray(args)           // true
args.constructor.name         // 'Array'
args instanceof Array         // true

Google Analytics' gtag.js expects the native Arguments object, which has different properties:

typeof arguments              // 'object'
Array.isArray(arguments)      // false
arguments.constructor.name    // 'Arguments'
arguments.callee              // Function reference (non-strict mode)

When gtag.js receives an Array instead of Arguments, it may:

  • Load Google Tag Manager mode (if GTM is configured)
  • Queue events without transmitting them
  • Silently fail without error messages

Correct Implementation

βœ… Right Implementation (Works Perfectly)

export function initGA(): void {
  window.dataLayer = window.dataLayer || [];
  
  // CRITICAL: Use arguments object, not spread operator
  window.gtag = function() {
    // eslint-disable-next-line prefer-rest-params
    window.dataLayer.push(arguments);
  };
  
  window.gtag('js', new Date());
  window.gtag('config', 'G-XXXXXXXXXX', {
    'send_page_view': false,
    'debug_mode': true,
    'transport_type': 'beacon',
  });
}

What Changes

  1. No parameters declared in the function signature
  2. Native arguments object used instead of spread operator
  3. ESLint exception added for prefer-rest-params rule

Results

  • βœ… Script loads successfully
  • βœ… window.gtag function available
  • βœ… Events added to dataLayer
  • βœ… Tracking beacons sent to google-analytics.com/g/collect
  • βœ… Real-time data visible in GA4 dashboard
  • βœ… Status 204 (No Content) responses from Google Analytics

Step-by-Step Setup

1. Create Analytics Utility File

File: src/utils/analytics.ts

/**
 * Google Analytics 4 integration
 * 
 * @see https://developers.google.com/analytics/devguides/collection/gtagjs
 */

const GA_MEASUREMENT_ID = import.meta.env.VITE_GA_MEASUREMENT_ID || 'G-XXXXXXXXXX';
let isInitialized = false;

/**
 * Initialize Google Analytics
 * Must be called once on app startup
 */
export function initGA(): void {
  // Prevent double initialization
  if (isInitialized) {
    return;
  }
  isInitialized = true;
  
  // Check if script already exists
  const existingScript = document.querySelector(
    `script[src*="googletagmanager.com/gtag/js?id=${GA_MEASUREMENT_ID}"]`
  );
  if (existingScript) {
    return;
  }

  // Validate measurement ID
  if (!GA_MEASUREMENT_ID || GA_MEASUREMENT_ID === 'G-XXXXXXXXXX') {
    console.warn('Google Analytics not configured. Set VITE_GA_MEASUREMENT_ID in .env');
    return;
  }

  // Load gtag.js script
  const script = document.createElement('script');
  script.async = true;
  script.src = `https://www.googletagmanager.com/gtag/js?id=${GA_MEASUREMENT_ID}`;
  document.head.appendChild(script);
  
  // Initialize gtag function - CRITICAL: use arguments, not spread operator
  window.dataLayer = window.dataLayer || [];
  window.gtag = function() {
    // eslint-disable-next-line prefer-rest-params
    window.dataLayer.push(arguments);
  };
  
  // Configure Google Analytics
  window.gtag('js', new Date());
  window.gtag('config', GA_MEASUREMENT_ID, {
    'send_page_view': false,        // Manual page view tracking
    'debug_mode': import.meta.env.DEV,  // Debug only in development
    'transport_type': 'beacon',     // Use sendBeacon API
  });
}

/**
 * Track page view
 * Call on route changes in SPA
 */
export function trackPageView(path: string, title: string): void {
  if (window.gtag) {
    window.gtag('event', 'page_view', {
      page_path: path,
      page_title: title,
    });
  }
}

/**
 * Track custom event
 */
export function trackEvent(eventName: string, eventParams?: Record<string, unknown>): void {
  if (window.gtag) {
    window.gtag('event', eventName, eventParams);
  }
}

// TypeScript declarations
declare global {
  interface Window {
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    dataLayer: any[];
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    gtag: (...args: any[]) => void;
  }
}

2. Environment Configuration

File: .env or .env.local

VITE_GA_MEASUREMENT_ID=G-BLABLABLA

3. Initialize in App Entry Point

File: src/main.tsx

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { initGA } from './utils/analytics';

// Initialize Google Analytics
initGA();

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

4. Track Page Views in Router

File: src/App.tsx (React Router example)

import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { trackPageView } from './utils/analytics';

function App() {
  const location = useLocation();

  useEffect(() => {
    // Track page view on route change
    const pageTitle = document.title;
    trackPageView(location.pathname, pageTitle);
  }, [location]);

  return (
    // Your app components
  );
}

Understanding the Arguments Object

What is arguments?

The arguments object is a special array-like object available in all JavaScript functions (except arrow functions). It contains all arguments passed to the function.

function example(a, b) {
  console.log(arguments[0]);        // First argument
  console.log(arguments[1]);        // Second argument
  console.log(arguments.length);    // Number of arguments
  console.log(arguments.callee);    // Reference to function itself
}

example(1, 2, 3, 4); 
// arguments = [1, 2, 3, 4] (array-like, not Array)

Arguments vs Array

Feature arguments Spread ...args
Type Arguments object Array
Array.isArray() ❌ false βœ… true
.map(), .filter() ❌ No βœ… Yes
Works with gtag.js βœ… Yes ❌ No
Modern syntax ❌ Legacy βœ… Modern
ESLint warning ⚠️ Yes βœ… Clean

Why GA4 Requires Arguments

Google's gtag.js library performs internal type checking to determine how to process the data. The exact implementation is minified, but evidence suggests:

// Pseudocode of what gtag.js might be doing
function processDataLayer(data) {
  if (isArgumentsObject(data)) {
    // Process as pure GA4 event
    sendToGoogleAnalytics(data);
  } else if (Array.isArray(data)) {
    // Process as GTM container event (queued, not sent)
    queueForTagManager(data);
  }
}

When you pass an Array, gtag.js may assume you're using Google Tag Manager, which requires additional configuration in the GA admin console.


Configuration Options

Essential Config Parameters

window.gtag('config', GA_MEASUREMENT_ID, {
  // Disable automatic page view tracking (recommended for SPAs)
  'send_page_view': false,
  
  // Enable debug mode in development
  'debug_mode': import.meta.env.DEV,
  
  // Force sendBeacon API for reliability
  'transport_type': 'beacon',
  
  // Optional: Cookie settings
  'cookie_flags': 'SameSite=None;Secure',
  'cookie_domain': 'auto',
  'cookie_expires': 63072000, // 2 years in seconds
  
  // Optional: Custom dimensions
  'custom_map': {
    'dimension1': 'user_type',
    'dimension2': 'subscription_tier',
  },
});

Transport Types

Value API Used Use Case
'beacon' navigator.sendBeacon() Recommended: Reliable, doesn't block navigation
'xhr' XMLHttpRequest Legacy browsers
'image' <img> pixel Fallback for oldest browsers
(default) fetch() with keepalive Modern default

Common Pitfalls

1. Arrow Functions

// ❌ WRONG: Arrow functions don't have arguments
window.gtag = () => {
  window.dataLayer.push(arguments); // ReferenceError!
};

// βœ… CORRECT: Use function expression
window.gtag = function() {
  window.dataLayer.push(arguments);
};

2. TypeScript Strict Mode

// ⚠️ TypeScript will complain about 'arguments'
function gtag() {
  window.dataLayer.push(arguments);
  // Error: 'arguments' is not defined
}

// βœ… Add ESLint exception
window.gtag = function() {
  // eslint-disable-next-line prefer-rest-params
  window.dataLayer.push(arguments);
};

3. React StrictMode Double Rendering

React 18+ StrictMode causes double renders in development, which may call initGA() twice:

// βœ… SOLUTION: Add initialization guard
let isInitialized = false;

export function initGA(): void {
  if (isInitialized) return;
  isInitialized = true;
  // ... rest of initialization
}

4. Localhost Tracking

GA4 does work on localhost (unlike Universal Analytics). No special configuration needed.

// ❌ NO NEED to disable on localhost
if (window.location.hostname === 'localhost') return;

// βœ… GA4 tracks localhost by default
initGA(); // Works everywhere

5. Ad Blockers

Ad blockers will block GA tracking. For development:

  • Use browser profiles without extensions
  • Or whitelist localhost in ad blocker settings
  • Check Network tab for blocked requests

Verification Checklist

After implementing, verify:

1. Network Tab (Chrome DevTools)

  • βœ… Request to googletagmanager.com/gtag/js?id=G-...
  • βœ… POST requests to google-analytics.com/g/collect
  • βœ… Status 204 (No Content) responses

2. Console

  • βœ… No errors
  • βœ… Optional: Check window.dataLayer structure
  • βœ… Optional: Check window.gtag is a function

3. Google Analytics Dashboard

  • βœ… Real-time users appear in Reports β†’ Realtime
  • βœ… Events appear in DebugView (if debug_mode enabled)
  • βœ… Page views tracked correctly

Key Takeaways

  1. Always use arguments, never spread operator for gtag initialization
  2. Add initialization guards to prevent double loading
  3. Use transport_type: 'beacon' for reliable tracking
  4. Disable automatic page views for SPAs, track manually
  5. GA4 works on localhost - no special config needed
  6. Test in browser profile without ad blockers during development
  7. Silent failures are common - always verify in Network tab

References

Last Updated: November 16, 2025