React Integration Example
This guide provides a complete, production-ready example of integrating Zenovay analytics into a React application using modern React patterns and hooks.
Quick Start
Add the Zenovay tracking script to your index.html:
<!-- public/index.html (Create React App) or index.html (Vite) -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>My React App</title>
</head>
<body>
<div id="root"></div>
<script defer data-tracking-code="YOUR_TRACKING_CODE" src="https://api.zenovay.com/z.js"></script>
</body>
</html>
That's it for basic page view tracking. The script automatically tracks page views on load.
Basic Setup
1. Create a Zenovay Hook
Create a reusable hook for tracking throughout your React app:
// hooks/useZenovay.ts
export function useZenovay() {
const track = (eventName: string, eventData: Record<string, any> = {}) => {
if (window.zenovay) {
window.zenovay('track', eventName, eventData);
}
};
const identify = (userId: string, traits: Record<string, any> = {}) => {
if (window.zenovay) {
window.zenovay('identify', userId, traits);
}
};
const trackGoal = (goalName: string, data: Record<string, any> = {}) => {
if (window.zenovay) {
window.zenovay('goal', goalName, data);
}
};
const trackPurchase = (data: { amount: number; currency: string; product: string }) => {
if (window.zenovay) {
window.zenovay('revenue', data.amount, data.currency);
}
};
return { track, identify, trackGoal, trackPurchase };
}
2. Track Page Views (React Router)
For single-page applications using React Router:
// App.tsx
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { useZenovay } from './hooks/useZenovay';
function PageTracker() {
const location = useLocation();
const { track } = useZenovay();
useEffect(() => {
track('pageview', {
path: location.pathname + location.search,
title: document.title,
});
}, [location.pathname]);
return null;
}
function App() {
return (
<Routes>
{/* Add PageTracker inside your router */}
<PageTracker />
{/* your routes */}
</Routes>
);
}
3. Track Custom Events
import { useZenovay } from './hooks/useZenovay';
function SignupButton() {
const { track } = useZenovay();
const handleSignup = () => {
track('signup_started', {
plan: 'professional',
source: 'pricing_page'
});
// Continue with signup logic
};
return <button onClick={handleSignup}>Start Free Trial</button>;
}
Complete Example
Here's a full implementation with all common use cases:
Page View Tracking Hook
// hooks/usePageTracking.ts
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
export function usePageTracking() {
const location = useLocation();
useEffect(() => {
// Track pageview on route change
if (window.zenovay) {
window.zenovay('track', 'pageview', {
path: location.pathname + location.search,
referrer: document.referrer,
title: document.title,
});
}
}, [location.pathname, location.search]);
}
Event Tracking Component
// components/TrackableButton.tsx
import { useZenovay } from '../hooks/useZenovay';
import { ButtonHTMLAttributes } from 'react';
interface TrackableButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
eventName: string;
eventData?: Record<string, any>;
children: React.ReactNode;
}
export function TrackableButton({
eventName,
eventData,
onClick,
children,
...props
}: TrackableButtonProps) {
const { track } = useZenovay();
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
// Track the event
track(eventName, eventData);
// Call original onClick if provided
onClick?.(e);
};
return (
<button onClick={handleClick} {...props}>
{children}
</button>
);
}
Usage Example
// pages/PricingPage.tsx
import { TrackableButton } from '../components/TrackableButton';
export function PricingPage() {
return (
<div>
<h1>Pricing</h1>
<TrackableButton
eventName="plan_selected"
eventData={{
plan: 'scale',
billing: 'monthly',
price: 90
}}
className="btn-primary"
>
Choose Scale
</TrackableButton>
</div>
);
}
Advanced Patterns
Conditional Tracking
Track events only when certain conditions are met:
import { useZenovay } from '../hooks/useZenovay';
import { useEffect } from 'react';
function ProductPage({ product }) {
const { track } = useZenovay();
useEffect(() => {
// Only track if product is high-value
if (product.price > 1000) {
track('high_value_product_viewed', {
product_id: product.id,
product_name: product.name,
price: product.price
});
}
}, [product]);
return <div>{/* product details */}</div>;
}
Form Tracking
Track form interactions and submissions:
import { useZenovay } from '../hooks/useZenovay';
import { useState, FormEvent } from 'react';
function ContactForm() {
const { track } = useZenovay();
const [formData, setFormData] = useState({
name: '',
email: '',
message: ''
});
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
// Track form submission
track('contact_form_submitted', {
form_id: 'contact',
has_message: formData.message.length > 0
});
// Submit form logic
};
const handleFocus = (field: string) => {
// Track field focus
track('form_field_focused', {
form_id: 'contact',
field_name: field
});
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
onFocus={() => handleFocus('name')}
placeholder="Name"
/>
<input
type="email"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
onFocus={() => handleFocus('email')}
placeholder="Email"
/>
<textarea
value={formData.message}
onChange={(e) => setFormData({ ...formData, message: e.target.value })}
onFocus={() => handleFocus('message')}
placeholder="Message"
/>
<button type="submit">Send Message</button>
</form>
);
}
E-commerce Tracking
Track product views, add to cart, and purchases:
import { useZenovay } from '../hooks/useZenovay';
function useEcommerceTracking() {
const { track, trackPurchase } = useZenovay();
const trackProductView = (product) => {
track('product_viewed', {
product_id: product.id,
product_name: product.name,
category: product.category,
price: product.price,
currency: 'USD'
});
};
const trackAddToCart = (product, quantity = 1) => {
track('product_added_to_cart', {
product_id: product.id,
product_name: product.name,
quantity,
price: product.price,
total: product.price * quantity
});
};
const trackCheckout = (cart) => {
track('checkout_started', {
cart_total: cart.total,
item_count: cart.items.length,
currency: 'USD'
});
};
const trackPurchaseCompleted = (order) => {
trackPurchase({
amount: order.total,
currency: 'USD',
product: `Order #${order.id}`,
});
track('purchase_completed', {
order_id: order.id,
revenue: order.total,
tax: order.tax,
shipping: order.shipping,
currency: 'USD',
items: order.items.map(item => ({
product_id: item.id,
quantity: item.quantity,
price: item.price
}))
});
};
return {
trackProductView,
trackAddToCart,
trackCheckout,
trackPurchaseCompleted
};
}
// Usage
function ProductPage({ product }) {
const { trackProductView, trackAddToCart } = useEcommerceTracking();
useEffect(() => {
trackProductView(product);
}, [product]);
return (
<div>
<h1>{product.name}</h1>
<p>${product.price}</p>
<button onClick={() => trackAddToCart(product)}>
Add to Cart
</button>
</div>
);
}
Error Boundary Tracking
Track errors automatically:
import { Component, ErrorInfo, ReactNode } from 'react';
interface Props {
children: ReactNode;
fallback?: ReactNode;
}
interface State {
hasError: boolean;
}
export class ErrorBoundary extends Component<Props, State> {
public state: State = {
hasError: false
};
public static getDerivedStateFromError(): State {
return { hasError: true };
}
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
// Track error via global zenovay object
if (window.zenovay) {
window.zenovay('track', 'error_occurred', {
error_message: error.message,
error_stack: error.stack,
component_stack: errorInfo.componentStack,
error_boundary: 'AppErrorBoundary'
});
}
console.error('Uncaught error:', error, errorInfo);
}
public render() {
if (this.state.hasError) {
return this.props.fallback || <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
User Identification
Track identified users:
import { useZenovay } from '../hooks/useZenovay';
import { useEffect } from 'react';
import { useAuth } from './auth-context';
function UserIdentifier() {
const { identify } = useZenovay();
const { user } = useAuth();
useEffect(() => {
if (user) {
identify(user.id, {
email: user.email,
name: user.name,
plan: user.subscription?.plan,
signup_date: user.createdAt
});
}
}, [user]);
return null;
}
// Add to your app layout
function App() {
return (
<>
<UserIdentifier />
{/* rest of app */}
</>
);
}
TypeScript Support
Full TypeScript support with typed events:
// types/analytics.ts
export interface AnalyticsEvents {
signup_started: {
plan: 'free' | 'pro' | 'scale' | 'enterprise';
source: string;
};
product_viewed: {
product_id: string;
product_name: string;
price: number;
};
purchase_completed: {
order_id: string;
revenue: number;
currency: string;
};
}
// Declare the global zenovay object
declare global {
interface Window {
zenovay: (...args: any[]) => void;
}
}
// hooks/useZenovay.ts - Typed version
import type { AnalyticsEvents } from '../types/analytics';
export function useZenovay() {
const track = <K extends keyof AnalyticsEvents>(
eventName: K,
data: AnalyticsEvents[K]
) => {
if (window.zenovay) {
window.zenovay('track', eventName, data);
}
};
const identify = (userId: string, traits: Record<string, any> = {}) => {
if (window.zenovay) {
window.zenovay('identify', userId, traits);
}
};
return { track, identify };
}
Performance Optimization
Debounce Tracking
Prevent excessive tracking:
import { useZenovay } from '../hooks/useZenovay';
import { useMemo } from 'react';
import { debounce } from 'lodash';
function SearchBox() {
const { track } = useZenovay();
const trackSearch = useMemo(
() =>
debounce((query: string) => {
track('search_performed', { query, length: query.length });
}, 500),
[]
);
return (
<input
type="search"
onChange={(e) => trackSearch(e.target.value)}
placeholder="Search..."
/>
);
}
Testing
Mock Zenovay in Tests
// test/setup.ts
beforeEach(() => {
// Mock the global zenovay function
window.zenovay = jest.fn();
});
Test Component
import { render, fireEvent } from '@testing-library/react';
import { TrackableButton } from './TrackableButton';
beforeEach(() => {
window.zenovay = jest.fn();
});
test('tracks event on click', () => {
const { getByText } = render(
<TrackableButton eventName="button_clicked" eventData={{ id: '123' }}>
Click Me
</TrackableButton>
);
fireEvent.click(getByText('Click Me'));
expect(window.zenovay).toHaveBeenCalledWith('track', 'button_clicked', { id: '123' });
});
Next.js App Router Example
Complete integration with Next.js 13+ App Router:
// app/layout.tsx
import Script from 'next/script';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
{children}
<Script
defer
data-tracking-code="YOUR_TRACKING_CODE"
src="https://api.zenovay.com/z.js"
strategy="afterInteractive"
/>
</body>
</html>
);
}
// app/analytics.tsx
'use client';
import { usePathname, useSearchParams } from 'next/navigation';
import { useEffect } from 'react';
export function Analytics() {
const pathname = usePathname();
const searchParams = useSearchParams();
useEffect(() => {
if (window.zenovay) {
window.zenovay('track', 'pageview', {
path: pathname,
title: document.title,
});
}
}, [pathname, searchParams]);
return null;
}
Environment Variables
# .env.local
# No special environment variables needed for Zenovay!
# The tracking code is embedded directly in the script tag.
# Simply replace YOUR_TRACKING_CODE with your actual tracking code
# from the Zenovay dashboard (app.zenovay.com).