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.gtagfunction 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
- No parameters declared in the function signature
- Native
argumentsobject used instead of spread operator - ESLint exception added for
prefer-rest-paramsrule
Results
- β Script loads successfully
- β
window.gtagfunction 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.dataLayerstructure - β
Optional: Check
window.gtagis 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
- Always use
arguments, never spread operator for gtag initialization - Add initialization guards to prevent double loading
- Use
transport_type: 'beacon'for reliable tracking - Disable automatic page views for SPAs, track manually
- GA4 works on localhost - no special config needed
- Test in browser profile without ad blockers during development
- Silent failures are common - always verify in Network tab
References
Last Updated: November 16, 2025
π¬ Comments & Reactions