[01] OpenCookies

@opencookies/react

React adapter — useConsent, useCategory, ConsentGate

React adapter for OpenCookies. Wraps @opencookies/core with useSyncExternalStore for concurrent-safe reactivity.

Install#

bun add @opencookies/core @opencookies/react

Peer dependencies: react >= 18.

Setup#

Wrap your app with <OpenCookiesProvider>:

import { OpenCookiesProvider } from "@opencookies/react";
import type { Category } from "@opencookies/core";
import { createRoot } from "react-dom/client";

const categories: Category[] = [
  { key: "essential", label: "Essential", locked: true },
  { key: "analytics", label: "Analytics" },
  { key: "marketing", label: "Marketing" },
];

createRoot(document.getElementById("root")!).render(
  <OpenCookiesProvider config={{ categories }}>
    <App />
  </OpenCookiesProvider>,
);

Pass a pre-created store with <OpenCookiesProvider store={store}> instead — useful for SSR-time hydration of decisions from cookies.

API#

useConsent()#

Returns the current consent state plus action methods. Re-renders the consumer when state changes.

import { useConsent } from "@opencookies/react";

function Banner() {
  const { route, acceptAll, acceptNecessary, setRoute } = useConsent();
  if (route !== "cookie") return null;

  return (
    <div className="banner">
      <button onClick={acceptNecessary}>Necessary only</button>
      <button onClick={acceptAll}>Accept all</button>
      <button onClick={() => setRoute("preferences")}>Customize</button>
    </div>
  );
}

useCategory(key)#

Granular per-category access. Returns { granted, toggle }.

import { useCategory } from "@opencookies/react";

function AnalyticsToggle() {
  const { granted, toggle } = useCategory("analytics");
  return (
    <label>
      <input type="checkbox" checked={granted} onChange={toggle} />
      Analytics
    </label>
  );
}

<ConsentGate>#

Renders children when the expression is satisfied; renders fallback otherwise. The component itself emits no DOM wrapper.

import { ConsentGate } from "@opencookies/react";

<ConsentGate requires="analytics" fallback={<EnablePrompt />}>
  <Chart />
</ConsentGate>;

<ConsentGate requires={{ and: ["analytics", "marketing"] }}>
  <PersonalizedPromo />
</ConsentGate>;

The requires shape is a ConsentExpr from core: a category key, { and: [...] }, { or: [...] }, or { not: ... }.

Next.js#

Mark the provider as a client component and mount it in your root layout:

// app/providers.tsx
"use client";
import { OpenCookiesProvider } from "@opencookies/react";
import type { Category } from "@opencookies/core";

const categories: Category[] = [
  { key: "essential", label: "Essential", locked: true },
  { key: "analytics", label: "Analytics" },
];

export function Providers({ children }: { children: React.ReactNode }) {
  return <OpenCookiesProvider config={{ categories }}>{children}</OpenCookiesProvider>;
}
// app/layout.tsx
import { Providers } from "./providers";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  );
}

For SSR-resolved decisions, build the store on the server with the cookie/header storage adapter and pass it via store={store} instead of config={config}.

Shared concepts#

Categories, GPC handling, jurisdiction resolvers, re-consent triggers, script gating (gateScript), and storage adapters all live in @opencookies/core — the React adapter is a thin reactivity wrapper. A working example is in examples/react.

See also#

License#

Apache-2.0