Skip to content

React Integration

Wrap your app (or the portion that needs consent) with ConsentProvider:

import { ConsentProvider } from "@uabcodus/consent/react";
function App() {
return (
<ConsentProvider
options={{
categories: {
necessary: { readOnly: true },
analytics: {},
marketing: {}
}
}}
>
<YourApp />
</ConsentProvider>
);
}

ConsentProvider creates a single ConsentInstance via useRef — the instance is stable across re-renders and never changes.

Place it high in your component tree, typically wrapping your entire app. You can have multiple providers if you need separate consent scopes (uncommon).

const consent = useConsent();

Returns the ConsentInstance. It also auto-subscribes to state changes — any component using useConsent re-renders whenever consent state updates.

import { useConsent } from "@uabcodus/consent/react";
function ConsentBanner() {
const consent = useConsent();
if (consent.state.valid) return null;
return (
<div>
<button onClick={() => consent.accept("all")}>Accept All</button>
<button onClick={() => consent.accept("necessary")}>Necessary Only</button>
</div>
);
}
function AnalyticsTracker() {
const consent = useConsent();
if (consent.acceptedCategory("analytics")) {
return <Script src="https://www.googletagmanager.com/gtag/js" />;
}
return null;
}

Throws if called outside a ConsentProvider.

Pass your category config type to ConsentProvider for full type inference:

const config = {
categories: {
necessary: { readOnly: true },
analytics: {},
marketing: {}
} as const
};
function App() {
return (
<ConsentProvider options={config}>
<ConsentBanner />
</ConsentProvider>
);
}
function ConsentBanner() {
const consent = useConsent();
// Fully typed: "necessary" | "analytics" | "marketing" | "all"
consent.accept("analytics");
return null;
}
function BannerOrTracker() {
const consent = useConsent();
if (!consent.state.valid) {
return <ConsentBanner />;
}
return <ConsentStatus />;
}
function PreferencesPanel() {
const consent = useConsent();
const [open, setOpen] = useState(false);
return (
<>
<button onClick={() => setOpen(true)}>Preferences</button>
<Dialog open={open} onClose={() => setOpen(false)}>
{Object.entries(consent.state.categories).map(([name, cat]) =>
cat.readOnly ? null : (
<label key={name}>
<input
type="checkbox"
checked={cat.accepted}
onChange={(e) => (e.target.checked ? consent.accept(name) : consent.reject(name))}
/>
{name}
</label>
)
)}
<button
onClick={() => {
consent.accept("all");
setOpen(false);
}}
>
Accept All
</button>
</Dialog>
</>
);
}

The hook triggers re-renders on every state change. If you need to run side effects without re-rendering, use the event system instead:

function GtagSyncer() {
const consent = useConsent();
useEffect(() => {
const unsub = consent.on("change", ({ cookie }) => {
window.gtag?.("consent", "update", {
analytics_storage: cookie.categories.includes("analytics") ? "granted" : "denied"
});
});
return unsub;
}, [consent]);
return null;
}

The library is client-side. If you’re server-rendering, make sure ConsentProvider only renders on the client (wrap it in a component with "use client" or use client:only in Astro).

On the initial render, consent.state.valid will be false until the cookie is read. This means your banner may flash on every page load if you’re not careful. To avoid this, read the cookie server-side and pass it as initialCookie:

// In your SSR setup:
const cookieValue = parseConsentCookie(cookies().get("cc_cookie"));
<ConsentProvider
options={{
initialCookie: cookieValue,
categories: {
/* ... */
}
}}
>
<App />
</ConsentProvider>;

This hydrates the consent state immediately — no flash, no flicker.