React dominates front-end development, but the framework itself has no opinion about cookie consent. That means the responsibility falls on you: loading the consent banner at the right time, blocking scripts until the visitor agrees, and sending accurate consent signals to Google, Meta, and your analytics stack.
This guide walks through the full implementation. It starts with the fundamentals (where to load the banner, how to manage consent state) and builds up to advanced patterns like conditional script loading and Consent Mode v2 integration. All examples use TypeScript and functional components.
If you want to skip the manual work entirely, the final section shows how CookieBeam handles all of this with a single script tag.
What this guide covers
- Where to place your consent banner in the React component tree
- Managing consent state with React Context
- Conditionally loading analytics and marketing scripts
- Google Consent Mode v2 integration
- Testing consent flows in development
- One-line CookieBeam setup for React apps
Why React apps need special handling
A static HTML site can block scripts by simply not including them until consent is given. React apps are different. The entire application is JavaScript, which means:
- Script timing matters. Third-party scripts loaded via
<script>tags inindex.htmlfire before React hydrates. If your consent banner is a React component, there is a window where scripts run without consent. - State is ephemeral. React state resets on page refresh unless you persist consent to
localStorageor cookies. A visitor who consented on the previous page load sees the banner again if you only store consent in React state. - SPAs do not reload. In a single-page app, navigation between routes does not trigger new script loads. Scripts loaded on consent must stay active across route changes, and scripts blocked before consent must not retroactively fire when consent is later given on a different route.
These constraints shape every decision below.
Step 1: Place the consent banner at the root
Your consent banner must render outside of route-level components. It needs to be visible on every page and persist across navigations. The standard pattern is to place it in your root layout component, alongside your router.
1 import { BrowserRouter, Routes, Route } from 'react-router-dom'; 2 import { ConsentProvider } from './consent/ConsentProvider'; 3 import { ConsentBanner } from './consent/ConsentBanner'; 4 import { HomePage } from './pages/HomePage'; 5 import { AboutPage } from './pages/AboutPage'; 6 7 export function App() { 8 return ( 9 <ConsentProvider> 10 <BrowserRouter> 11 <Routes> 12 <Route path="/" element={<HomePage />} /> 13 <Route path="/about" element={<AboutPage />} /> 14 </Routes> 15 <ConsentBanner /> 16 </BrowserRouter> 17 </ConsentProvider> 18 ); 19 }
The ConsentProvider wraps the entire app so any component can read the current consent state. The ConsentBanner sits outside the Routes so it stays mounted during navigation.
Step 2: Manage consent state with React Context
You need a consent context that does three things: reads persisted consent from storage on mount, exposes the current state to the rest of the app, and writes consent decisions back to storage when the visitor interacts with the banner.
1 import { createContext, useContext, useState, useEffect, useCallback } from 'react'; 2 3 type ConsentCategories = { 4 necessary: boolean; 5 analytics: boolean; 6 marketing: boolean; 7 preferences: boolean; 8 }; 9 10 type ConsentState = { 11 categories: ConsentCategories; 12 decided: boolean; 13 update: (categories: ConsentCategories) => void; 14 }; 15 16 const STORAGE_KEY = 'cookie_consent'; 17 18 const defaults: ConsentCategories = { 19 necessary: true, 20 analytics: false, 21 marketing: false, 22 preferences: false, 23 }; 24 25 const ConsentContext = createContext<ConsentState | null>(null); 26 27 export function useConsent() { 28 const ctx = useContext(ConsentContext); 29 if (!ctx) throw new Error('useConsent must be used inside ConsentProvider'); 30 return ctx; 31 } 32 33 export function ConsentProvider({ children }: { children: React.ReactNode }) { 34 const [categories, setCategories] = useState<ConsentCategories>(defaults); 35 const [decided, setDecided] = useState(false); 36 37 useEffect(() => { 38 try { 39 const stored = localStorage.getItem(STORAGE_KEY); 40 if (stored) { 41 setCategories(JSON.parse(stored)); 42 setDecided(true); 43 } 44 } catch { 45 // Storage unavailable or corrupted; show banner 46 } 47 }, []); 48 49 const update = useCallback((next: ConsentCategories) => { 50 const safe = { ...next, necessary: true }; 51 setCategories(safe); 52 setDecided(true); 53 localStorage.setItem(STORAGE_KEY, JSON.stringify(safe)); 54 }, []); 55 56 return ( 57 <ConsentContext.Provider value={{ categories, decided, update }}> 58 {children} 59 </ConsentContext.Provider> 60 ); 61 }
Always force necessary: true
The necessary category covers cookies that the site needs to function (session tokens, CSRF tokens, load balancers). Never let a visitor disable it. The example above forces necessary: true on every update, regardless of what the banner sends.
Step 3: Conditionally load third-party scripts
With the consent context in place, you can gate script loading on specific consent categories. The key principle: do not render the script tag at all until consent is given.
1 import { useEffect } from 'react'; 2 import { useConsent } from './ConsentProvider'; 3 4 type Props = { 5 category: 'analytics' | 'marketing' | 'preferences'; 6 src: string; 7 id?: string; 8 }; 9 10 export function ConsentScript({ category, src, id }: Props) { 11 const { categories, decided } = useConsent(); 12 13 useEffect(() => { 14 if (!decided || !categories[category]) return; 15 16 // Avoid injecting duplicates on re-render 17 const existingId = id ?? `consent-script-${src}`; 18 if (document.getElementById(existingId)) return; 19 20 const script = document.createElement('script'); 21 script.src = src; 22 script.async = true; 23 script.id = existingId; 24 document.head.appendChild(script); 25 }, [decided, categories, category, src, id]); 26 27 return null; 28 }
Use this component anywhere in your tree to conditionally load a script:
<ConsentScript
category="analytics"
src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXX"
/>
<ConsentScript
category="marketing"
src="https://connect.facebook.net/en_US/fbevents.js"
/>Step 4: Integrate Google Consent Mode v2
Google requires Consent Mode v2 signals for EEA traffic if you use Google Ads or GA4. The consent defaults must be set before the Google tag loads, and updated when the visitor makes a choice.
The correct order is:
- Set default consent state (denied for EEA visitors)
- Load the Google tag
- Update consent state when the visitor interacts with the banner
1 import { useEffect } from 'react'; 2 import { useConsent } from './ConsentProvider'; 3 4 declare global { 5 interface Window { 6 dataLayer: Array<Record<string, unknown>>; 7 gtag: (...args: unknown[]) => void; 8 } 9 } 10 11 function gtag(...args: unknown[]) { 12 window.dataLayer = window.dataLayer || []; 13 window.dataLayer.push(arguments); 14 } 15 16 export function GoogleConsentMode() { 17 const { categories, decided } = useConsent(); 18 19 // Set defaults once on mount 20 useEffect(() => { 21 gtag('consent', 'default', { 22 analytics_storage: 'denied', 23 ad_storage: 'denied', 24 ad_user_data: 'denied', 25 ad_personalization: 'denied', 26 functionality_storage: 'granted', 27 security_storage: 'granted', 28 wait_for_update: 500, 29 }); 30 }, []); 31 32 // Update when consent changes 33 useEffect(() => { 34 if (!decided) return; 35 gtag('consent', 'update', { 36 analytics_storage: categories.analytics ? 'granted' : 'denied', 37 ad_storage: categories.marketing ? 'granted' : 'denied', 38 ad_user_data: categories.marketing ? 'granted' : 'denied', 39 ad_personalization: categories.marketing ? 'granted' : 'denied', 40 }); 41 }, [decided, categories]); 42 43 return null; 44 }
Why wait_for_update matters
The wait_for_update: 500 parameter tells Google tags to wait up to 500ms for a consent update before processing hits. This gives your React app time to hydrate, read stored consent, and fire the update call. Without it, the first pageview fires with denied consent even if the visitor previously consented.
Step 5: Build the consent banner component
The banner reads from the consent context and calls update() with the visitor's choices. Here is a minimal implementation:
1 import { useState } from 'react'; 2 import { useConsent } from './ConsentProvider'; 3 4 export function ConsentBanner() { 5 const { decided, update } = useConsent(); 6 const [showSettings, setShowSettings] = useState(false); 7 const [draft, setDraft] = useState({ 8 analytics: false, 9 marketing: false, 10 preferences: false, 11 }); 12 13 if (decided) return null; 14 15 const acceptAll = () => 16 update({ necessary: true, analytics: true, marketing: true, preferences: true }); 17 18 const rejectAll = () => 19 update({ necessary: true, analytics: false, marketing: false, preferences: false }); 20 21 const saveChoices = () => 22 update({ necessary: true, ...draft }); 23 24 return ( 25 <div role="dialog" aria-label="Cookie consent" style={bannerStyle}> 26 <p>We use cookies to improve your experience and measure site usage.</p> 27 {showSettings ? ( 28 <div> 29 <label> 30 <input type="checkbox" checked disabled /> Necessary (always on) 31 </label> 32 <label> 33 <input 34 type="checkbox" 35 checked={draft.analytics} 36 onChange={(e) => setDraft({ ...draft, analytics: e.target.checked })} 37 /> 38 Analytics 39 </label> 40 <label> 41 <input 42 type="checkbox" 43 checked={draft.marketing} 44 onChange={(e) => setDraft({ ...draft, marketing: e.target.checked })} 45 /> 46 Marketing 47 </label> 48 <button onClick={saveChoices}>Save preferences</button> 49 </div> 50 ) : ( 51 <div> 52 <button onClick={acceptAll}>Accept all</button> 53 <button onClick={rejectAll}>Reject all</button> 54 <button onClick={() => setShowSettings(true)}>Customize</button> 55 </div> 56 )} 57 </div> 58 ); 59 } 60 61 const bannerStyle: React.CSSProperties = { 62 position: 'fixed', 63 bottom: 0, 64 left: 0, 65 right: 0, 66 padding: '1rem', 67 background: '#fff', 68 borderTop: '1px solid #e5e7eb', 69 zIndex: 9999, 70 };
Step 6: Test the consent flow
Before deploying, verify these scenarios:
Testing checklist
Banner appears on first visit
Clear localStorage and reload. The banner should appear.
Banner disappears after consent
Click Accept All. The banner should vanish and not reappear on refresh.
Scripts load only after consent
Open the Network tab. Verify analytics/marketing scripts are absent before consent and present after.
Consent persists across sessions
Close the browser, reopen, and navigate to the site. The banner should not appear if consent was previously given.
Reject All blocks everything
Click Reject All. Verify no analytics or marketing scripts load on any page.
Consent Mode defaults fire first
In the dataLayer, verify the consent default event appears before any config or event tags.
Settings button works
A returning visitor should be able to change their consent by clicking a settings button (floating widget or footer link).
The easy way: use CookieBeam
The code above works, but you are building and maintaining consent infrastructure from scratch. You need to handle edge cases (cookie expiry, regional detection, TCF compliance, consent logging for auditors) that the examples above do not cover.
CookieBeam replaces all of the code above with a single script tag:
<script src="https://cdn.cookiebeam.com/banner/YOUR_BANNER_ID/default/loader.js" async></script>That single line handles:
- Automatic consent state management with cookie-based persistence (works across tabs and sessions)
- Google Consent Mode v2 defaults and updates, including
ad_user_dataandad_personalization - Microsoft Consent Mode for Clarity and Bing Ads
- Meta Pixel consent signals
- Regional detection so visitors in different jurisdictions see the legally correct banner (GDPR opt-in for the EEA, opt-out for US states, no banner where not required)
- Cookie scanning and classification to keep your cookie declaration accurate
- Consent logging with timestamps, exportable for audits
- Script blocking that prevents third-party scripts from firing until the right category is consented to
If you are using a framework like React, Vite, or Create React App, the script tag goes in your index.html before the closing </head> tag. For server-side rendered frameworks, check our Next.js guide.
React-specific gotchas
A few issues that are unique to React apps:
- StrictMode double-mounts. In development, React 18+ mounts components twice in StrictMode. Your script injection code must be idempotent (check for existing script elements before injecting). The
ConsentScriptexample above handles this with an ID check. - Hydration mismatches in SSR. If you server-render your app (Remix, Gatsby SSR), the consent state on the server is always "undecided" because there is no localStorage. The banner must render client-side only to avoid a hydration mismatch.
- Context not available in head. The
<head>section is not inside your React tree, so you cannot useuseConsent()there. Load Google Consent Mode defaults via a plain<script>tag inindex.htmlinstead, and handle updates from inside the React tree. - Lazy-loaded routes. If a route is code-split and loads its own analytics script, the consent check must happen inside that route component, not at the root. The
ConsentScriptpattern works for this because it checks consent on mount.
Frequently asked questions
Do I need a cookie consent banner for a React SPA?
Yes, if your React app sets cookies or uses tracking scripts (Google Analytics, Meta Pixel, Hotjar) and serves visitors in the EU, UK, or US states with privacy laws. The legal requirement is tied to the cookies and tracking, not the framework.
Can I use a React cookie consent library instead of building my own?
You can, but most open-source React consent libraries only show a banner and store a boolean. They do not handle Google Consent Mode v2, regional detection, script blocking, or consent logging. If you need compliance-grade consent (not just a banner), a dedicated consent management platform is the better choice.
Where should I store consent state in a React app?
localStorage is the simplest option and works for most cases. For cross-subdomain support, use a first-party cookie instead. Do not store consent only in React state, because it resets on page refresh.
How does CookieBeam work with React Router?
CookieBeam loads as a standalone script outside your React tree, so it works with any router (React Router, TanStack Router, or plain browser navigation). It persists consent in a first-party cookie, monitors for new scripts injected during SPA navigation, and blocks them if the required consent category has not been granted.
Does the consent banner affect my Core Web Vitals?
A well-implemented banner has minimal impact. CookieBeam loads asynchronously and does not block rendering. The banner itself uses CSS transforms for animation, which avoids layout shifts (CLS). Loading it with the async attribute keeps it off the critical rendering path.
Summary
Adding cookie consent to a React app requires three pieces: a consent state manager (Context + localStorage), conditional script loading, and Consent Mode v2 integration. You can build this yourself with the code above, or use CookieBeam to handle everything with a single script tag.
For related guides, see Next.js Cookie Consent with App Router, Google Consent Mode v2, and Complete Banner Setup Guide.