React
@consentloop/react is a thin, tree-shakeable layer over the core: a provider, a hook, and a gate component. The widget itself stays vanilla — no React in the render path, no hydration cost, SSR-safe by construction.
npm i consentloop @consentloop/react
Provider
// app/providers.tsx (Next.js: a client component)
"use client";
import { ConsentProvider } from "@consentloop/react";
export function Providers({ children }) {
return (
<ConsentProvider
config={{
categories: {
necessary: { required: true },
analytics: { services: { ga4: { label: "Google Analytics 4" } } },
marketing: {},
},
googleConsentMode: true,
}}
>
{children}
</ConsentProvider>
);
}
The provider calls ConsentLoop.run() in an effect — nothing renders on the server, nothing blocks hydration. Strict-mode double-invoke is handled (run is idempotent).
useConsent()
import { useConsent } from "@consentloop/react";
function CookieSettingsLink() {
const { showPreferences } = useConsent();
return <button onClick={showPreferences}>Cookie settings</button>;
}
function AnalyticsInfo() {
const { consent, isAccepted } = useConsent(); // re-renders on every consent change
if (!consent) return null; // no decision yet
return <p>Analytics: {isAccepted("analytics") ? "on" : "off"}</p>;
}
Implemented with useSyncExternalStore — concurrent-mode safe, no tearing, null snapshot during SSR.
<ConsentGate>
The React-idiomatic way to gate embeds: the real element mounts only after consent, with a designed placeholder before.
import { ConsentGate, useConsent } from "@consentloop/react";
function Video({ id }) {
const { accept } = useConsent();
return (
<ConsentGate
category="marketing"
fallback={
<div className="video-placeholder">
<p>This video is hosted on YouTube.</p>
<button onClick={() => accept(["marketing"])}>Load video & accept marketing cookies</button>
</div>
}
>
<iframe src={`https://www.youtube.com/embed/${id}`} title="Video" />
</ConsentGate>
);
}
Next.js App Router recipe
// app/layout.tsx
import { Providers } from "./providers";
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}
💡Analytics scripts in Next.js: keep using
next/script? Gate via services instead — onAccept: () => import("./ga") — or plain <script type="text/plain" data-consent> tags in app/layout. Both patterns play well with streaming.Headless React UI
Want your own banner in JSX? Run headless and drive everything from the hook — full recipe in Headless / custom UI:
<ConsentProvider config={{ ui: false, categories: { analytics: {} } }}>
<MyOwnBanner /> {/* uses useConsent().accept / rejectAll / consent */}
</ConsentProvider>