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.
Recommended React GA4 Architecture
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.tsruns 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_viewautomatically, but for client-side routing (React Router, Next.js, etc.), you must manually fire it on navigation. A component withuseEffectlistening 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:
- Detects existing gtag scripts via
findGtagScriptOnPage【/cache/repos/github.com/firebase/firebase-js-sdk/main/packages/analytics/src/helpers.ts#L33-L45】 - Inserts the script tag if missing
- Queues the
gtag('js', …)andgtag('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
useAnalyticshook for safe instance access, and aPageViewTrackercomponent to handle React Router navigation. - Initialize once by calling
getAnalyticsin your config module; the SDK automatically prevents duplicate script tags usingfindGtagScriptOnPageinpackages/analytics/src/helpers.ts. - Track navigation manually because GA4 only sends the initial
page_viewautomatically; uselogEventfrompackages/analytics/src/api.tson route changes. - Configure advanced features like custom gtag names, default parameters, user properties, and consent settings via the
SettingsOptionsinterface 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.
What is the best way to handle GDPR or CCPA consent in React GA4 implementations?
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →