React SDK
Integrate ConsentStack into React and Next.js apps with hooks and components.
React bindings for ConsentStack. Use hooks to read consent state, conditionally render components, and build custom consent UIs, all with full TypeScript support.
Installation
npm install @consentstack/reactPeer dependencies: react and react-dom ^18 or ^19.
For best performance, also add <link rel="preconnect" href="https://cdn.consentstack.io" /> to the document <head> (e.g. via your framework's head/metadata API). The React loader injects the SDK script tags itself, but warming the TLS connection to the CDN first cuts time-to-banner.
Quick start
Choose the approach that fits your needs:
| Approach | When to use |
|---|---|
ConsentStack | You just need the banner. No hooks, no conditional rendering. |
ConsentStackProvider | You need hooks to read consent state, conditionally load scripts, or build a custom UI. |
Simple integration
Install the package
pnpm add @consentstack/reactAdd to your layout
import { ConsentStack } from '@consentstack/react'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
{children}
<ConsentStack siteKey="<YOUR_SITE_KEY>" />
</body>
</html>
)
}That's it. The consent banner renders automatically.
With hooks
Wrap your app with the provider
import { ConsentStackProvider } from '@consentstack/react'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<ConsentStackProvider siteKey="<YOUR_SITE_KEY>">
{children}
</ConsentStackProvider>
</body>
</html>
)
}Use consent in any component
import { useConsent } from '@consentstack/react'
function CookieStatus() {
const { hasConsent, showPreferences, error } = useConsent()
if (error) return <p>Failed to load consent: {error.message}</p>
return (
<div>
<p>Analytics: {hasConsent('analytics') ? 'Allowed' : 'Denied'}</p>
<button onClick={() => showPreferences()}>
Manage preferences
</button>
</div>
)
}Component props
ConsentStack
| Prop | Type | Default | Description |
|---|---|---|---|
siteKey | string | required | Your site's public key from the ConsentStack dashboard. |
debug | boolean | false | Log SDK initialization and consent events to the console. |
cdnUrl | string | "https://cdn.consentstack.io/consent.js" | Override the SDK script URL (useful for self-hosting). |
timeout | number | 15000 | Timeout in milliseconds for SDK script loading. |
ConsentStackProvider
Accepts all ConsentStack props plus:
| Prop | Type | Default | Description |
|---|---|---|---|
mode | "banner" | "headless" | "banner" | "banner" shows the consent UI automatically. "headless" hides it so you can build your own. |
children | ReactNode | required | Your app tree. |
Hooks
useConsent
Returns the full consent state and control methods. Must be used within a ConsentStackProvider.
interface UseConsentReturn {
/**
* Effective consent per category (explicit decision, GPC, or implicit
* grant by consent model), or null if not yet known
*/
consent: Record<string, boolean> | null
/** The loaded consent config, or null if not loaded */
config: ConsentConfig | null
/** Resolved region for the current visitor (e.g. "US-CA", "gdpr"), or null if unresolved */
region: string | null
/** True while the SDK is initializing */
isLoading: boolean
/** True once consent state is known and no error occurred */
isReady: boolean
/**
* Whether an explicit consent decision exists (stored from a prior visit
* or made this session). Use this, not `consent === null`, to decide
* whether to render a custom banner: `consent` is effective consent and
* is non-null even before the user has chosen.
*/
hasDecision: boolean
/** Error if SDK failed to load, null otherwise */
error: Error | null
/** Update consent programmatically, logs to the server */
setConsent: (categories: Record<string, boolean>) => Promise<void>
/** Check if a specific category has consent */
hasConsent: (category: string) => boolean
/** Programmatically show the consent banner */
showBanner: () => void
/** Open the preferences panel */
showPreferences: () => void
/** Close the preferences panel */
hidePreferences: () => void
/** Whether the preferences panel is currently open */
isPreferencesOpen: boolean
}A few fields deserve a closer look:
consentis effective consent: the same per-category state the SDK's request blocker acts on. In an opt-out region it istruefor non-essential categories before the visitor touches the banner; in an opt-in region it isfalseuntil they grant. It is not "null until the user decides".isReadymeans consent state is known, not merely that the script loaded. With the standard install, consent state is seeded from the edge, soisReadyistrueas soon as the provider connects to the SDK, without waiting for a config fetch or a user decision. Gating onisReady && hasConsent('analytics')is the recommended pattern and works immediately on the landing page.hasDecisiontells you whether an explicit choice exists. If you are building a custom banner, show it whenhasDecisionisfalse.
The config object uses the ConsentConfig type from the JS SDK. See the JavaScript API reference for the full shape.
The region field is the resolved region string for the current visitor (e.g. "US-CA", "gdpr", or null if unresolved). Used to drive region-aware UI such as the California-specific privacy choices link rendered by <PrivacyChoicesButton /> (see packages/react/src/components.tsx:72-77).
useConsentValue
An optimized hook for checking a single consent category. Uses useSyncExternalStore under the hood, so your component only re-renders when that specific category value changes.
function useConsentValue(category: string): booleanReturns true if the category has consent, false otherwise. Returns false during SSR and while the SDK is loading, a safe default that prevents scripts from firing before consent is confirmed.
import { useConsentValue } from '@consentstack/react'
function AnalyticsLoader() {
const hasAnalytics = useConsentValue('analytics')
if (!hasAnalytics) return null
return <script src="https://www.googletagmanager.com/gtag/js?id=G-XXXXX" async />
}useConsentEvent
Subscribe to SDK lifecycle events. Automatically subscribes on mount and unsubscribes on unmount. You don't need to memoize the callback. The hook handles that internally via a ref.
function useConsentEvent<T extends ConsentEventType>(
event: T,
callback: (data: ConsentEventData[T]) => void
): void| Event | Payload | Fires when |
|---|---|---|
"ready" | { config, consent, effectiveConsent, hasDecision } | SDK has initialized and consent state is known. consent is the stored explicit decision (or null), effectiveConsent is the state the request blocker acts on. |
"consent" | Record<string, boolean> | Effective consent changes, including init reconciliation when initialization resolves a state that differs from what was known before. |
"error" | Error | An SDK error occurs, including init aborts (site not configured, domain not registered for the site key). |
"preferences:open" | undefined | Preferences panel opens. |
"preferences:close" | undefined | Preferences panel closes. |
import { useConsentEvent } from '@consentstack/react'
function PreferencesTracker() {
useConsentEvent('preferences:open', () => {
analytics.track('consent_preferences_opened')
})
useConsentEvent('error', (error) => {
console.error('SDK error:', error)
})
return null
}Error handling
The provider exposes errors from SDK initialization. If the CDN is unreachable, the script fails to load, or the SDK aborts init (site not configured, domain not registered for the site key), error will contain the reason. When error is set, isReady stays false.
function ConsentFallback() {
const { error, isReady } = useConsent()
if (error) {
return <p>Consent system unavailable. Some features may be limited.</p>
}
if (!isReady) return null
return <YourApp />
}The SDK times out after 15 seconds by default. If the CDN is slow or blocked, isLoading will become false and error will contain a descriptive timeout message. Customize the timeout with the timeout prop on ConsentStackProvider or ConsentStack.
Common patterns
First-page analytics
Gate landing page events on isReady && hasConsent('analytics'):
import { useEffect } from 'react'
import { useConsent } from '@consentstack/react'
function PageViewTracker() {
const { isReady, hasConsent } = useConsent()
useEffect(() => {
if (isReady && hasConsent('analytics')) {
trackPageView()
}
}, [isReady, hasConsent])
return null
}With the standard install, consent state is seeded from the edge, so the gate passes as soon as the provider connects to the SDK, without waiting for a config fetch or a user decision. Landing page events from visitors whose consent model grants analytics (implicitly or from a stored decision) are no longer lost while the SDK initializes.
Conditional script loading
Block third-party scripts until the visitor grants consent:
import { useConsentValue } from '@consentstack/react'
import Script from 'next/script'
function ThirdPartyScripts() {
const analyticsAllowed = useConsentValue('analytics')
const marketingAllowed = useConsentValue('marketing')
return (
<>
{analyticsAllowed && (
<Script src="https://www.googletagmanager.com/gtag/js?id=G-XXXXX" strategy="afterInteractive" />
)}
{marketingAllowed && (
<Script src="https://connect.facebook.net/en_US/fbevents.js" strategy="afterInteractive" />
)}
</>
)
}Custom preferences UI (headless mode)
Set mode="headless" on the provider to suppress the default banner entirely. Then build your own UI using the hook methods:
<ConsentStackProvider siteKey="<YOUR_SITE_KEY>" mode="headless">
{children}
</ConsentStackProvider>import { useConsent } from '@consentstack/react'
function CookiePreferences() {
const { consent, config, setConsent, isReady, showPreferences, hidePreferences } = useConsent()
if (!isReady || !config) return null
const handleToggle = (categoryId: string, enabled: boolean) => {
setConsent({ ...consent, [categoryId]: enabled })
}
return (
<div>
<h2>Cookie Preferences</h2>
{config.categories.map((cat) => (
<label key={cat.id}>
<input
type="checkbox"
checked={consent?.[cat.id] ?? cat.default}
disabled={cat.required}
onChange={(e) => handleToggle(cat.id, e.target.checked)}
/>
<span>{cat.name}</span>
<p>{cat.description}</p>
</label>
))}
</div>
)
}To decide when your custom banner should appear, use hasDecision. Checking consent === null no longer works: consent is effective consent and is populated even before the visitor makes a choice.
function CustomBanner() {
const { isReady, hasDecision, setConsent } = useConsent()
// Only show the banner when no explicit decision exists yet
if (!isReady || hasDecision) return null
return (
<div role="dialog">
<p>We use cookies for analytics and marketing.</p>
<button onClick={() => setConsent({ essential: true, analytics: true, marketing: true })}>
Accept all
</button>
<button onClick={() => setConsent({ essential: true, analytics: false, marketing: false })}>
Reject all
</button>
</div>
)
}In headless mode, you can call showBanner() to display the default ConsentStack banner on demand. This is useful as a fallback while you build your custom UI.
TypeScript
All exports are fully typed. The key types you can import:
import type {
ConsentConfig,
ConsentCategory,
ConsentStackAPI,
ConsentEventType,
ConsentEventData,
UseConsentReturn,
ConsentStackProps,
ConsentStackProviderProps,
} from '@consentstack/react'ConsentConfig, ConsentCategory, ConsentStackAPI, ConsentEventType, and ConsentEventData are re-exported from the JS SDK. The React package is always in sync.
What's next
- Script blocking: how ConsentStack prevents scripts from firing before consent
- Categories and regions: configuring which consent categories apply to which regions
- JavaScript API: full reference for the underlying SDK