Consent management with JKT48Connect in Next.js
Privacy regulations like GDPR and CCPA require that you obtain explicit user consent before tracking behaviour or collecting user data. This guide shows how to gate your JKT48Connect analytics integration behind a consent check in Next.js — holding all tracking until the user makes a choice, then enabling everything at once — or discarding it silently on decline.
Prerequisites
- A Next.js project (App Router or Pages Router)
- Your JKT48Connect API key from the JKT48Connect portal
Initialise with consent check
Create a client-side module that reads the stored consent choice before initialising the JKT48Connect client. If consent hasn't been granted yet, set a flag to disable tracking so no data leaves the browser prematurely.
'use client';
const BASE_URL = 'https://v2.jkt48connect.com';
const API_KEY = process.env.NEXT_PUBLIC_JKT48_API_KEY!;
let trackingEnabled = false;
const eventQueue: Array<{ endpoint: string; payload?: object }> = [];
export function isTrackingEnabled() {
return trackingEnabled;
}
export async function jkt48Track(endpoint: string, payload?: object) {
if (!trackingEnabled) {
eventQueue.push({ endpoint, payload });
return;
}
await sendEvent(endpoint, payload);
}
async function sendEvent(endpoint: string, payload?: object) {
await fetch(`${BASE_URL}${endpoint}?apikey=${API_KEY}`, {
method: 'GET',
...payload,
});
}
export function enableTracking() {
trackingEnabled = true;
// Flush all queued events
while (eventQueue.length > 0) {
const item = eventQueue.shift()!;
sendEvent(item.endpoint, item.payload);
}
}From this point on, any jkt48Track(...) calls elsewhere in your app are safely queued and not transmitted until consent is given.
Build a consent banner
Create a Client Component for the consent UI. The key is to call enableTracking() when the user accepts, and do nothing when they decline.
'use client';
import { useEffect, useState } from 'react';
import { enableTracking } from '@/lib/jkt48';
export function ConsentBanner() {
const [visible, setVisible] = useState(false);
useEffect(() => {
const stored = localStorage.getItem('jkt48_consent');
if (!stored) setVisible(true);
if (stored === 'granted') enableTracking();
}, []);
function handleAccept() {
localStorage.setItem('jkt48_consent', 'granted');
enableTracking(); // flushes the queue and enables all future tracking
setVisible(false);
}
function handleDecline() {
localStorage.setItem('jkt48_consent', 'denied');
setVisible(false); // queue is discarded, nothing is sent
}
if (!visible) return null;
return (
<div role="dialog" aria-label="Cookie consent">
<p>
We use JKT48Connect to display JKT48 data in this app. Do you consent
to anonymous usage tracking?
</p>
<button type="button" onClick={handleAccept}>Accept</button>
<button type="button" onClick={handleDecline}>Decline</button>
</div>
);
}Call enableTracking() on consent
enableTracking() does two things:
- Sets the
trackingEnabledflag so all future fetch calls are sent immediately - Flushes the entire queue — every event buffered since the module was loaded is sent at once
This means you don't lose any events that happened before the user made their choice. Any data requests made while the banner was visible are captured and sent the moment they consent.
Mount the banner in your layout
Add the ConsentBanner to your root layout so it appears on every page. Because it's a Client Component, it won't affect server rendering.
App Router
import { ConsentBanner } from '@/components/ConsentBanner';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
{children}
<ConsentBanner />
</body>
</html>
);
}Pages Router
import type { AppProps } from 'next/app';
import { ConsentBanner } from '@/components/ConsentBanner';
export default function App({ Component, pageProps }: AppProps) {
return (
<>
<Component {...pageProps} />
<ConsentBanner />
</>
);
}Handle decline
If the user declines, don't call enableTracking(). The queue lives only in memory and is automatically discarded when the tab closes or the page navigates away. No data is ever sent to the JKT48Connect API.
function handleDecline() {
localStorage.setItem('jkt48_consent', 'denied');
// trackingEnabled stays false — nothing will be sent
// The in-memory queue will be garbage collected
}Persist consent across page loads
The trackingEnabled flag resets on every page load because Next.js re-imports modules fresh on navigation. You need to read the stored consent choice inside a useEffect on initialisation and call enableTracking() immediately if consent was already granted.
The ConsentBanner component already handles this in its useEffect:
useEffect(() => {
const stored = localStorage.getItem('jkt48_consent');
if (!stored) setVisible(true); // no choice yet — show banner
if (stored === 'granted') enableTracking(); // already accepted — enable immediately
}, []);This ensures returning visitors never see the banner again and tracking resumes instantly without re-prompting.
Full example
'use client';
const BASE_URL = 'https://v2.jkt48connect.com';
const API_KEY = process.env.NEXT_PUBLIC_JKT48_API_KEY!;
let trackingEnabled = false;
const eventQueue: Array<{ endpoint: string; payload?: object }> = [];
export function enableTracking() {
trackingEnabled = true;
while (eventQueue.length > 0) {
const item = eventQueue.shift()!;
fetch(`${BASE_URL}${item.endpoint}?apikey=${API_KEY}`);
}
}
export async function jkt48Fetch(endpoint: string) {
if (!trackingEnabled) {
eventQueue.push({ endpoint });
return null;
}
const res = await fetch(`${BASE_URL}${endpoint}?apikey=${API_KEY}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}'use client';
import { useEffect, useState } from 'react';
import { enableTracking } from '@/lib/jkt48';
export function ConsentBanner() {
const [visible, setVisible] = useState(false);
useEffect(() => {
const stored = localStorage.getItem('jkt48_consent');
if (!stored) setVisible(true);
if (stored === 'granted') enableTracking();
}, []);
if (!visible) return null;
return (
<div role="dialog" aria-label="Cookie consent">
<p>We use JKT48Connect to power this app. Do you consent?</p>
<button
type="button"
onClick={() => {
localStorage.setItem('jkt48_consent', 'granted');
enableTracking();
setVisible(false);
}}
>
Accept
</button>
<button
type="button"
onClick={() => {
localStorage.setItem('jkt48_consent', 'denied');
setVisible(false);
}}
>
Decline
</button>
</div>
);
}import { ConsentBanner } from '@/components/ConsentBanner';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
{children}
<ConsentBanner />
</body>
</html>
);
}Related
- JKT48Connect API docs — full endpoint reference
- Next.js Client Components — when and how to use
'use client' - next/headers cookies — server-side alternative for storing consent in cookies


