This guide covers both the App Router (Next.js 13+) and Pages Router (Next.js 12 and earlier).
Install the Package
npm install @grainql/analytics-web
yarn add @grainql/analytics-web
pnpm add @grainql/analytics-web
App Router (Next.js 13+)
Create a Provider Component
The Grain provider uses React Context, so it needs to be a Client Component:
// app/providers.tsx
'use client';
import { GrainProvider } from '@grainql/analytics-web/react';
import { ReactNode } from 'react';
export function Providers({ children }: { children: ReactNode }) {
return (
<GrainProvider
config={{
tenantId: process.env.NEXT_PUBLIC_GRAIN_TENANT_ID!,
defaultConfigurations: {
hero_text: 'Welcome!'
}
}}
>
{children}
</GrainProvider>
);
}
Wrap Your Root Layout
// app/layout.tsx
import { Providers } from './providers';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<Providers>
{children}
</Providers>
</body>
</html>
);
}
Add Environment Variable
Create a .env.local file:
NEXT_PUBLIC_GRAIN_TENANT_ID=your-tenant-id
Replace your-tenant-id with the alias from your dashboard.
Use in Components
Now you can use Grain hooks in any Client Component:
// app/page.tsx
'use client';
import { useTrack, useConfig } from '@grainql/analytics-web/react';
export default function HomePage() {
const track = useTrack();
const { value: heroText } = useConfig('hero_text');
return (
<div>
<h1>{heroText || 'Welcome!'}</h1>
<button onClick={() => track('cta_clicked', { location: 'hero' })}>
Get Started
</button>
</div>
);
}
Server vs Client Components: Grain hooks must be used in Client Components (with 'use client' directive). For Server Components, see the server-side tracking section below.
Track Page Views
Create a client component to track route changes:
// app/analytics-tracker.tsx
'use client';
import { useEffect } from 'react';
import { usePathname, useSearchParams } from 'next/navigation';
import { useTrack } from '@grainql/analytics-web/react';
export function AnalyticsTracker() {
const pathname = usePathname();
const searchParams = useSearchParams();
const track = useTrack();
useEffect(() => {
track('page_viewed', {
page: pathname,
search: searchParams.toString(),
title: document.title
});
}, [pathname, searchParams, track]);
return null;
}
Add it to your layout:
// app/layout.tsx
import { Providers } from './providers';
import { AnalyticsTracker } from './analytics-tracker';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<Providers>
<AnalyticsTracker />
{children}
</Providers>
</body>
</html>
);
}
Pages Router (Next.js 12)
Wrap Your App
// pages/_app.tsx
import type { AppProps } from 'next/app';
import { GrainProvider } from '@grainql/analytics-web/react';
export default function App({ Component, pageProps }: AppProps) {
return (
<GrainProvider
config={{
tenantId: process.env.NEXT_PUBLIC_GRAIN_TENANT_ID!,
defaultConfigurations: {
hero_text: 'Welcome!'
}
}}
>
<Component {...pageProps} />
</GrainProvider>
);
}
Add Environment Variable
Create a .env.local file:
NEXT_PUBLIC_GRAIN_TENANT_ID=your-tenant-id
Use in Pages
// pages/index.tsx
import { useTrack, useConfig } from '@grainql/analytics-web/react';
export default function HomePage() {
const track = useTrack();
const { value: heroText } = useConfig('hero_text');
return (
<div>
<h1>{heroText || 'Welcome!'}</h1>
<button onClick={() => track('cta_clicked', { location: 'hero' })}>
Get Started
</button>
</div>
);
}
Track Page Views
Track route changes with Next.js Router:
// pages/_app.tsx
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import { GrainProvider } from '@grainql/analytics-web/react';
import { useTrack } from '@grainql/analytics-web/react';
function AnalyticsTracker() {
const router = useRouter();
const track = useTrack();
useEffect(() => {
const handleRouteChange = (url: string) => {
track('page_viewed', {
page: url,
title: document.title
});
};
router.events.on('routeChangeComplete', handleRouteChange);
// Track initial page
handleRouteChange(router.pathname);
return () => {
router.events.off('routeChangeComplete', handleRouteChange);
};
}, [router, track]);
return null;
}
export default function App({ Component, pageProps }: AppProps) {
return (
<GrainProvider config={{ tenantId: process.env.NEXT_PUBLIC_GRAIN_TENANT_ID! }}>
<AnalyticsTracker />
<Component {...pageProps} />
</GrainProvider>
);
}
Server-Side Tracking
Track events from API routes or Server Components:
API Routes
// app/api/checkout/route.ts (App Router)
import { createGrainAnalytics } from '@grainql/analytics-web';
const grain = createGrainAnalytics({
tenantId: process.env.GRAIN_TENANT_ID!,
authStrategy: 'SERVER_SIDE',
secretKey: process.env.GRAIN_SECRET_KEY!
});
export async function POST(request: Request) {
const body = await request.json();
// Track server-side event
await grain.track('checkout_completed', {
order_id: body.orderId,
total: body.total
}, { flush: true }); // Flush immediately for serverless
return Response.json({ success: true });
}
Serverless tip: Use { flush: true } to send events immediately before the function terminates.
Server Actions (App Router)
// app/actions.ts
'use server';
import { createGrainAnalytics } from '@grainql/analytics-web';
const grain = createGrainAnalytics({
tenantId: process.env.GRAIN_TENANT_ID!,
authStrategy: 'SERVER_SIDE',
secretKey: process.env.GRAIN_SECRET_KEY!
});
export async function submitForm(formData: FormData) {
// Your form logic...
await grain.track('form_submitted', {
form_name: 'contact'
}, { flush: true });
return { success: true };
}
Complete Example
Here’s a full Next.js App Router example:
// app/providers.tsx
'use client';
import { GrainProvider } from '@grainql/analytics-web/react';
export function Providers({ children }: { children: React.ReactNode }) {
return (
<GrainProvider config={{
tenantId: process.env.NEXT_PUBLIC_GRAIN_TENANT_ID!
}}>
{children}
</GrainProvider>
);
}
// app/layout.tsx
import { Providers } from './providers';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}
// app/page.tsx
'use client';
import { useTrack, useConfig } from '@grainql/analytics-web/react';
export default function HomePage() {
const track = useTrack();
const { value: heroText } = useConfig('hero_text');
const { value: ctaText } = useConfig('cta_text');
return (
<div>
<h1>{heroText || 'Welcome to Next.js + Grain'}</h1>
<button onClick={() => track('cta_clicked', { location: 'hero' })}>
{ctaText || 'Get Started'}
</button>
</div>
);
}
What’s Next?
Vercel deployment: All environment variables starting with NEXT_PUBLIC_ are automatically available in the browser. Keep secret keys (for server-side tracking) private by omitting the NEXT_PUBLIC_ prefix.