Next.js

Last updated:

|Edit this page
Which features are available in this library?
  • Event capture
  • Autocapture
  • User identification
  • Session recording
  • Feature flags
  • Group analytics

PostHog makes it easy to get data about traffic and usage of your Next.js app. Integrating PostHog into your site enables analytics about user behavior, custom events capture, session recordings, feature flags, and more.

This guide walks you through integrating PostHog into your Next.js app using the React and the Node.js SDKs.

You can see a working example of this integration in our Next.js demo app.

Next.js has both client and server-side rendering, as well as pages and app routers. We'll cover all of these options in this guide.

Prerequisites

To follow this guide along, you need:

  1. A PostHog instance (either Cloud or self-hosted)
  2. A Next.js application

Client-side setup

Install posthog-js using your package manager:

Terminal
yarn add posthog-js
# or
npm install --save posthog-js
# or
pnpm add posthog-js

Add your environment variables to your .env.local file and to your hosting provider (e.g. Vercel, Netlify, AWS). You can find your project API key in your project settings.

.env.local
NEXT_PUBLIC_POSTHOG_KEY=<ph_project_api_key>
NEXT_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com

These values need to start with NEXT_PUBLIC_ to be accessible on the client-side.

Pages router

If your Next.js app uses the pages router, you can integrate PostHog at the root of your app (pages/_app.js).

JavaScript
// pages/_app.js
import { useEffect } from 'react'
import { useRouter } from 'next/router'
import posthog from 'posthog-js'
import { PostHogProvider } from 'posthog-js/react'
// Check that PostHog is client-side (used to handle Next.js SSR)
if (typeof window !== 'undefined') {
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY, {
api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST || 'https://us.i.posthog.com',
person_profiles: 'identified_only',
// Enable debug mode in development
loaded: (posthog) => {
if (process.env.NODE_ENV === 'development') posthog.debug()
}
})
}
export default function App({ Component, pageProps }) {
const router = useRouter()
useEffect(() => {
// Track page views
const handleRouteChange = () => posthog?.capture('$pageview')
router.events.on('routeChangeComplete', handleRouteChange)
return () => {
router.events.off('routeChangeComplete', handleRouteChange)
}
}, [])
return (
<PostHogProvider client={posthog}>
<Component {...pageProps} />
</PostHogProvider>
)
}

App router

If your Next.js app to uses the app router, you can integrate PostHog by creating a providers file in your app folder. This is because the posthog-js library needs to be initialized on the client-side using the Next.js 'use client' directive.

// app/providers.js
'use client'
import posthog from 'posthog-js'
import { PostHogProvider } from 'posthog-js/react'
if (typeof window !== 'undefined') {
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY, {
api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
person_profiles: 'identified_only',
capture_pageview: false // Disable automatic pageview capture, as we capture manually
})
}
export function PHProvider({ children }) {
return <PostHogProvider client={posthog}>{children}</PostHogProvider>
}

PostHog's $pageview autocapture relies on page load events. Since Next.js acts as a single-page app, this event doesn't trigger on navigation and we need to capture $pageview events manually.

To do this, we set up a PostHogPageView component to listen to URL path changes:

// app/PostHogPageView.jsx
'use client'
import { usePathname, useSearchParams } from "next/navigation";
import { useEffect } from "react";
import { usePostHog } from 'posthog-js/react';
export default function PostHogPageView() {
const pathname = usePathname();
const searchParams = useSearchParams();
const posthog = usePostHog();
useEffect(() => {
// Track pageviews
if (pathname && posthog) {
let url = window.origin + pathname
if (searchParams.toString()) {
url = url + `?${searchParams.toString()}`
}
posthog.capture(
'$pageview',
{
'$current_url': url,
}
)
}
}, [pathname, searchParams, posthog])
return null
}

Then, import the PHProvider component into your app/layout file and wrap your app with it. We also dynamically import the PostHogPageView component and include it as a child of PHProvider.

Why is PostHogPageView dynamically imported? It contains the useSearchParams hook, which deopts the entire app into client-side rendering if it is not dynamically imported.

// app/layout.js
import './globals.css'
import { PHProvider } from './providers'
import dynamic from 'next/dynamic'
const PostHogPageView = dynamic(() => import('./PostHogPageView'), {
ssr: false,
})
export default function RootLayout({ children }) {
return (
<html lang="en">
<PHProvider>
<body>
<PostHogPageView />
{children}
</body>
</PHProvider>
</html>
)
}

PostHog is now set up and ready to go. Files and components accessing PostHog on the client-side need the 'use client' directive.

Pageleave events (optional)

To capture pageleave events, we need to set capture_pageleave: true in the initialization because setting capture_pageview: false disables it.

// app/providers.js
'use client'
import posthog from 'posthog-js'
import { PostHogProvider } from 'posthog-js/react'
if (typeof window !== 'undefined') {
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY, {
api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
person_profiles: 'identified_only',
capture_pageview: false,
capture_pageleave: true // Enable pageleave capture
})
}
export function PHProvider({ children }) {
return <PostHogProvider client={posthog}>{children}</PostHogProvider>
}

Accessing PostHog using the provider

PostHog can then be accessed throughout your Next.js app by using the usePostHog hook. See the React SDK docs for examples of how to use:

You can also read the full posthog-js documentation for all the usable functions.

Server-side analytics

Server-side rendering enables you to render pages on the server instead of the client. This can be useful for SEO, performance, and user experience.

To integrate PostHog into your Next.js app on the server-side, you can use the Node SDK.

First, install the posthog-node library:

Terminal
yarn add posthog-node
# or
npm install --save posthog-node

Pages router

For the pages router, we can use the getServerSideProps function to access PostHog on the server-side, send events, evaluate feature flags, and more.

This looks like this:

JavaScript
// pages/posts/[id].js
import { useContext, useEffect, useState } from 'react'
import { getServerSession } from "next-auth/next"
import { PostHog } from 'posthog-node'
export default function Post({ post, flags }) {
const [ctaState, setCtaState] = useState()
useEffect(() => {
if (flags) {
setCtaState(flags['blog-cta'])
}
})
return (
<div>
<h1>{post.title}</h1>
<p>By: {post.author}</p>
<p>{post.content}</p>
{ctaState &&
<p><a href="/">Go to PostHog</a></p>
}
<button onClick={likePost}>Like</button>
</div>
)
}
export async function getServerSideProps(ctx) {
const session = await getServerSession(ctx.req, ctx.res)
let flags = null
if (session) {
const client = new PostHog(
process.env.NEXT_PUBLIC_POSTHOG_KEY,
{
host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
}
)
flags = await client.getAllFlags(session.user.email);
client.capture({
distinctId: session.user.email,
event: 'loaded blog article',
properties: {
$current_url: ctx.req.url,
},
});
await client.shutdown()
}
const { posts } = await import('../../blog.json')
const post = posts.find((post) => post.id.toString() === ctx.params.id)
return {
props: {
post,
flags
},
}
}

Note: Make sure to always call await client.shutdown() after sending events from the server-side. PostHog queues events into larger batches, and this call forces all batched events to be flushed immediately.

App router

For the app router, we can initialize the posthog-node SDK once with a PostHogClient function, and import it into files.

This enables us to send events and fetch data from PostHog on the server – without making client-side requests.

JavaScript
// app/posthog.js
import { PostHog } from 'posthog-node'
export default function PostHogClient() {
const posthogClient = new PostHog(process.env.NEXT_PUBLIC_POSTHOG_KEY, {
host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
flushAt: 1,
flushInterval: 0
})
return posthogClient
}

Note: Because our server-side posthog-node initializations are short-lived, we set flushAt to 1 and flushInterval to 0. flushAt sets how many capture calls we should flush the queue (in one batch). flushInterval sets how many milliseconds we should wait before flushing the queue. Setting them to the lowest number ensures events are sent immediately and not batched. We also need to call await posthog.shutdown() once done.

JavaScript
import Link from 'next/link'
import PostHogClient from '../posthog'
export default async function About() {
const posthog = PostHogClient()
const flags = await posthog.getAllFlags(
'user_distinct_id' // replace with a user's distinct ID
);
await posthog.shutdown()
return (
<main>
<h1>About</h1>
<Link href="/">Go home</Link>
{ flags['main-cta'] &&
<Link href="http://posthog.com/">Go to PostHog</Link>
}
</main>
)
}

Configuring a reverse proxy to PostHog

To improve the reliability of client-side tracking and make requests less likely to be intercepted by tracking blockers, you can setup a reverse proxy in Next.js. Read more about deploying a reverse proxy using Next.js rewrites, Next.js middleware, and Vercel rewrites.

Further reading

Questions?

  • Aleksei
    a month ago

    pageleave events snippet for pages router is missing oldUrl assignment?

    This code snippet for page leaves in app router introduces oldUrlRef. However it never assigns to it, this seems wrong https://posthog.com/docs/libraries/next-js#pageleave-events-optional

    • dani
      15 days ago

      I assumed that is a mistake from page and made

      const router = useRouter()
      const oldUrlRef = useRef(router)
  • Muhammad
    a month ago

    Wrong import

    It says to use in providers.jsx but it isnt exported from PostHogPageView.jsx

    You need to use

    • Ahmed
      a month ago

      ... NVM, I just realised I commented on the wrong post, sorry!

  • Ahmed
    a month ago

    Hydration error on Nextjs 14.0.4 app router

    While using Posthog in app router, I get Error: Hydration failed because the initial UI does not match what was rendered on the server..

    I'm dynamically importing the PostHogView component in the PostHogProvider as suggested in the docs (for < Next 15). What am I doing wrong?

    // posthog-pageview.tsx
    "use client";
    import { usePathname, useSearchParams } from "next/navigation";
    import { useEffect } from "react";
    import { usePostHog } from "posthog-js/react";
    export default function PostHogPageView(): null {
    const pathname = usePathname();
    const searchParams = useSearchParams();
    const posthog = usePostHog();
    // Track pageviews
    useEffect(() => {
    if (pathname && posthog) {
    let url = window.origin + pathname;
    if (searchParams.toString()) {
    url = url + `?${searchParams.toString()}`;
    }
    posthog.capture("$pageview", { $current_url: url });
    }
    }, [pathname, searchParams, posthog]);
    return null;
    }
    // posthog-provider.tsx
    "use client";
    import React from "react";
    import posthog from "posthog-js";
    import dynamic from "next/dynamic";
    import { PostHogProvider as PHProvider } from "posthog-js/react";
    import { isDevelopmentEnv } from "@/lib/utils";
    const PostHogPageView = dynamic(() => import("@/components/posthog-pageview"), {
    ssr: false,
    });
    export function PostHogProvider({ children }: { children: React.ReactNode }) {
    if (isDevelopmentEnv) return children;
    React.useEffect(() => {
    console.log("🚀 ~ React.useEffect ~ isDevelopmentEnv:", isDevelopmentEnv);
    const posthogKey = process.env.NEXT_PUBLIC_POSTHOG_KEY;
    if (!posthogKey) {
    throw new Error("NEXT_PUBLIC_POSTHOG_KEY is not defined");
    }
    posthog.init(posthogKey, {
    api_host: "/ingest",
    capture_pageview: false, // Disable automatic pageview capture, as we capture manually
    });
    }, []);
    return (
    <PHProvider client={posthog}>
    <PostHogPageView />
    {children}
    </PHProvider>
    );
    }
  • Lewis
    3 months ago

    Syntax errors

    Some issues with the app router example:

    • export default SuspendedPostHogPageView() { is missing function after default.
    • You didn't import Suspense.
    • Importing PostHogProvider errors because that's the same name as your component.
    • Rafael(he/him)
      3 months agoSolution

      Hey, Lewis!

      You're absolutely right, thanks for catching these. I've opened a PR with the fix already, which means this should be updated in ~20min.

  • Petr
    4 months ago

    Mismatch between docs and intro setup in Posthog

    I just registered to Posthog and it gave me legacy instructions without useEffect for tracking in Next.js pages router during onboarding. Will this be fixed? :)

    • Joshua
      18 days ago

      Hi @petr/32208, all fixed, thanks for pointing this out!

  • Maicon
    4 months ago

    Always invalidating cache using fetch

    It seems we have an issue here.

    The cache from API calls using the native fetch is always invalidated Cache skipped reason: (cache-control: no-cache (hard refresh)) when you don't use the useEffect inside the provider and fetching some API endpoint from a page.

    Maybe the doc needs to be updated. I'm using "next": "15.0.3" with the app folder.

    Here is my solution to prevent that behavior. And preventing the provider from being executed in development too:

    'use client'
    import { useEffect } from 'react'
    import posthog from 'posthog-js'
    import { PostHogProvider } from 'posthog-js/react'
    const POSTHOG_KEY = process.env.NEXT_PUBLIC_POSTHOG_KEY
    const POSTHOG_HOST = process.env.NEXT_PUBLIC_POSTHOG_HOST
    export default function PostHogClientProvider({ children, isProduction }) {
    if (!isProduction) return children
    useEffect(() => {
    posthog.init(POSTHOG_KEY, {
    api_host: POSTHOG_HOST,
    person_profiles: 'identified_only',
    capture_pageview: false,
    capture_pageleave: true,
    })
    }, [])
    return <PostHogProvider client={posthog}>{children}</PostHogProvider>
    }
    • Maicon
      Author4 months ago

      I was wrong, even using the useEffect, the cache is always invalidated.

  • Aaron
    5 months ago

    Can I lazy load the posthog-js lib.

    Will passing an uninit'ed posthog instance into the provider be an issue. It will be init'd once the posthog-js lib loads (its 50kb so not exactly small)

  • Graham
    5 months ago

    Next 15 RC2

    Looks like there's a breaking change for ssr: false in dynamic imports in Next 15 RC2. What do you recommend going forward?

    https://github.com/vercel/next.js/pull/70378

    • Zev
      5 months agoSolution

      It seems like recommended way is to wrap the PostHogPageView component in a suspense boundary?

      <Suspense fallback={null}>
      <PostHogPageView />
      </Suspense>

      given it will go up the tree until it finds suspense wrapping it directly should solve the issue ?

  • acomms
    5 months ago

    API Route Event Tracking?

    Any special considerations for those calling events in an api router on the nextjs pages router?

    Currently, in an api call, I am initializing client, making capture event calls, then calling shutdown. This all gets done on each api call to a route with a tracked event. Is this the most efficient implementation?

  • Artem
    6 months ago

    Incorrect recording of the number of actions

    I have strangely counted actions(Next.js 14 ,app router) The number of one action is constantly changing, it can first fix one, then 4 appears and then it can decrease. The effect works on the client once. What could it be?

    • Paul(he/him)
      6 months ago

      Can you share a little more? Where are you seeing this count? What's the action definition?

      thanks

  • Mack
    7 months ago

    Module posthog-js/react not found.

    I am experiencing the error Cannot find module 'posthog-js/react' or its corresponding type declarations. I did installed the latest version available of posthog-js package, I see it's installed and included in package.json but this error is there. How can I fix it?

  • Waldo
    7 months ago

    Stop Vercel Reverse Proxy from localhost:3000

    Hey PostHog community, I'm using NextJs and I configured the reverse proxy with Vercel, but I don't want this to run in my localhost development computer, how do I stop it?

    Screenshot 2024-08-14 at 12.02.53 AM.png

    • Sitanshu
      7 months agoSolution

      if (location.hostname === "localhost" || location.hostname === "127.0.0.1") { posthog.opt_out_capturing(); }

      use this in your providers.tsx

  • Kenroy
    8 months ago

    Updated /app/provider.tsx

    If you are running into issues with Next.js and hydrating errors because the server code differs from the client, make sure to add a useEffect instead of checking for the windows object.

    Change:

    "use client";
    import posthog from "posthog-js";
    import { PostHogProvider } from "posthog-js/react";
    if (typeof window !== "undefined") {
    posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
    api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
    person_profiles: "identified_only",
    capture_pageview: false,
    capture_pageleave: true, // Enable pageleave capture
    });
    }
    export function PHProvider({ children }: { children: React.ReactNode }) {
    return <PostHogProvider client={posthog}>{children}</PostHogProvider>;
    }

    into

    "use client";
    import posthog from "posthog-js";
    import { PostHogProvider } from "posthog-js/react";
    import { useEffect } from "react";
    export function PHProvider({ children }: { children: React.ReactNode }) {
    useEffect(() => {
    posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
    api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
    person_profiles: "identified_only",
    capture_pageview: false,
    capture_pageleave: true,
    });
    }, []);
    return <PostHogProvider client={posthog}>{children}</PostHogProvider>;
    }
  • klaas
    9 months ago

    Flush events on page leave

    I get the feeling we are missing events in posthog because of unsynced events. Is there an option to manually flush events from the queue, that can be triggered on moments that I know are important to have all events?

  • Jonathan
    9 months ago

    I'd like to A/B test with Server Components

    Ideally I would set up a provider that works both on the server and on the client and can use a hook that also works on both. It should also work with anonymous users. I'm not sure if I should use the client side setup above combined with the server side setup or if this use case is not supported.

  • Dave
    9 months ago

    useSearchParams error building on Vercel

    The client-side instructions here worked perfectly on my local build, but when I push to Vercel I get build errors as follows:

    useSearchParams() should be wrapped in a suspense boundary at page "/404". Read more: https://nextjs.org/docs/messages/missing-suspense-with-csr-bailout
    at o (/home/runner/work/XXXX/XXXX/web/.next/server/chunks/15.js:1:10810)
    ...
    Error occurred prerendering page "/_not-found". Read more: https://nextjs.org/docs/messages/prerender-error
    useSearchParams() should be wrapped in a suspense boundary at page "/". Read more: https://nextjs.org/docs/messages/missing-suspense-with-csr-bailout
    at o (/home/runner/work/XXXX/XXXX/web/.next/server/chunks/15.js:1:10810)
    ...
    Error occurred prerendering page "/". Read more: https://nextjs.org/docs/messages/prerender-error
    Generating static pages (5/5)
    > Export encountered errors on following paths:
    /_not-found/page: /_not-found
    /page: /
    Error: Command "npm run build" exited with 1

    What am I missing? Thanks!

    • Lukas
      8 months agoSolution

      Probably the dynamic import

      const PostHogPageView = dynamic(() => import('./PostHogPageView'), {
      ssr: false,
      })
  • Ali
    a year ago

    add url

    How can I add the URL to the event when using server-side tracking?

    PostHogClient().capture({
    distinctId: "...",
    event: "page_view",
    });

    Event-explorer-•-PostHog.png

    • nils
      a year agoSolution

      nevermind, I had named the file .ts not .tsx ....

  • Ali
    a year ago

    No events are shown off

    I followed the docs (Next.js app router) to setup my first project with PostHog, but I can't observe any view events being monitored!

  • Cody
    a year ago
    • Marcus
      a year agoSolution

      Hey Cody, you can just install the posthog-js package which contains the react part aswell.

  • Matt
    2 years ago

    Warning: Prop `dangerouslySetInnerHTML` did not match

    When I add the PHProvider to my RootLayout component I start to get a warning about client and server props for dangerouslySetInnerHTML not matching. When I remove the provider everything continues to work as normal. I'm also using a NextAuth Provider and Next-Theme provider which are rendered as children of PHProvider and are within the body of the application.

    • Marcus
      2 years agoSolution

      Quick note: That is a Warning and will not impact functionality.

Was this page useful?

Next article

Nuxt.js

PostHog makes it easy to get data about usage of your Nuxt.js app. Integrating PostHog into your app enables analytics about user behavior, custom events capture, session replays, feature flags, and more. These docs are aimed at Nuxt.js users who run Nuxt in spa or universal mode. You can see a working example of the Nuxt v3.0 integration in our Nuxt.js demo app Nuxt v3.0 and above Setting up PostHog on the client side Install posthog-js using your package manager: Add your PostHog API…

Read next article