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>