Docs
Integrations
Next.js

Next.js Integration

Use a client wrapper in App Router, then mount FeatureDropProvider once near the root.

App Router pattern (app/)

app/providers.tsx
'use client'
 
import { LocalStorageAdapter } from 'featuredrop'
import { FeatureDropProvider } from 'featuredrop/react'
import type { FeatureManifest } from 'featuredrop'
 
export function Providers({
  children,
  manifest,
  appVersion
}: {
  children: React.ReactNode
  manifest: FeatureManifest
  appVersion: string
}) {
  return (
    <FeatureDropProvider
      manifest={manifest}
      storage={new LocalStorageAdapter({ prefix: 'featuredrop' })}
      appVersion={appVersion}
    >
      {children}
    </FeatureDropProvider>
  )
}
app/layout.tsx
import { Providers } from './providers'
import { manifest } from '@/featuredrop.manifest'
 
export default function RootLayout({ children }: { children: React.ReactNode }) {
  const appVersion = process.env.NEXT_PUBLIC_APP_VERSION ?? '0.0.0'
 
  return (
    <html lang="en">
      <body>
        <Providers manifest={manifest} appVersion={appVersion}>
          {children}
        </Providers>
      </body>
    </html>
  )
}

Where to mount UI components

Mount long-lived UI once (for example in your root layout shell):

  • <ChangelogWidget />
  • global <Toast />
  • <Tour /> if it should persist across route transitions

Using remote state in Next.js

If you persist dismissals remotely, use RemoteAdapter with an API route:

import { RemoteAdapter } from 'featuredrop/adapters'
 
new RemoteAdapter({ url: '/api/featuredrop' })

Expected endpoints:

  • GET /api/featuredrop
  • GET /api/featuredrop/state
  • POST /api/featuredrop/dismiss
  • POST /api/featuredrop/dismiss-batch (optional, for burst dismiss traffic)
  • POST /api/featuredrop/dismiss-all

Minimal App Router handlers:

app/api/featuredrop/dismiss-batch/route.ts
import { NextResponse } from 'next/server'
 
export async function POST(req: Request) {
  const body = (await req.json()) as { featureIds?: string[] }
  const featureIds = Array.isArray(body.featureIds) ? body.featureIds.filter(Boolean) : []
  if (!featureIds.length) return NextResponse.json({ ok: true })
 
  // Persist to your DB by user/session.
  await saveDismissBatch(featureIds)
  return NextResponse.json({ ok: true })
}
app/api/featuredrop/dismiss/route.ts
import { NextResponse } from 'next/server'
 
export async function POST(req: Request) {
  const body = (await req.json()) as { featureId?: string }
  if (!body.featureId) return NextResponse.json({ error: 'featureId required' }, { status: 400 })
 
  await saveDismiss(body.featureId)
  return NextResponse.json({ ok: true })
}
⚠️

Do not create a new storage adapter instance inside frequently re-rendering leaf components. Keep adapter/provider setup stable at app-shell level.