Part of the Iframe Embedding & Isolation guide.
Operative rule: Never inject Folium HTML directly into the React DOM — always isolate it inside a sandboxed <iframe> using a Blob URL, so Leaflet’s scripts run in a separate document context and cannot pollute your application’s global namespace or event system.
How It Works
Folium outputs a complete, self-contained HTML document: inline <script> blocks, Leaflet initialisation routines, and external tile server references are all bundled together. React’s reconciliation engine expects predictable JSX trees. Mixing these two models with dangerouslySetInnerHTML bypasses React’s built-in XSS mitigations and breaks Leaflet’s DOM assumptions simultaneously.
The correct architecture, covered fully in the Iframe Embedding & Isolation cluster, converts the Folium HTML string into a Blob, wraps it in a temporary object URL, and points an <iframe src={blobUrl}> at that URL. The iframe executes in its own document context — a distinct origin for Blob URLs — so Leaflet cannot reach the parent window’s globals, stylesheets, or event listeners. The parent React app communicates back through postMessage, leaving the rendering boundary intact.
This pattern integrates naturally into broader Python-to-Web Generation Workflows where Python builds the HTML artifact and the React frontend receives it as a string prop — no bundler re-compilation, no additional Webpack loaders.
The diagram below shows the three-layer boundary: Python server, Blob URL origin, and React app origin.
Why Direct DOM Injection Fails
Attempting to render Folium output with dangerouslySetInnerHTML produces four categories of failure:
- Tile rendering failures: Leaflet expects a clean, unmodified container. React’s virtual DOM diffing can detach or recreate nodes mid-initialisation, causing blank tiles or flickering on every re-render.
- Global variable collisions: Leaflet, D3, or custom map plugins write to
window. Multiple maps or third-party React libraries can overwrite shared namespaces, triggering uncaught reference errors that are difficult to reproduce. - CSS namespace leaks: Folium’s inline styles and Leaflet’s default stylesheet cascade into the React app, breaking layout grids, typography, and component theming.
- Event handler conflicts: Inline
onclickoronloadattributes in Folium HTML execute outside React’s synthetic event system, creating memory leaks and untracked listeners that survive component unmount.
The isolation pattern described here eliminates all four categories by giving Leaflet its own document context that React never touches.
Production-Ready Implementation
The component below is the standard production variant. It handles Blob creation, cleanup on unmount, and graceful loading state — with correct sandbox directives for Folium’s tile requests.
import React, { useEffect, useRef, useState } from "react";
/**
* FoliumMapEmbed — renders a Folium-generated HTML string inside
* a sandboxed iframe using a Blob URL. Revokes the URL on unmount
* or when htmlContent changes to prevent memory leaks.
*/
const FoliumMapEmbed = ({ htmlContent, width = "100%", height = "420px" }) => {
const iframeRef = useRef(null);
const [blobUrl, setBlobUrl] = useState(null);
useEffect(() => {
if (!htmlContent) return;
// 1. Wrap the Folium HTML string in a Blob.
// type: "text/html" is required — without it, browsers refuse to render.
const blob = new Blob([htmlContent], { type: "text/html" });
const url = URL.createObjectURL(blob);
setBlobUrl(url);
// 2. Revoke the object URL when htmlContent changes or the component unmounts.
// Orphaned Blob URLs accumulate in memory and degrade long-lived dashboards.
return () => {
URL.revokeObjectURL(url);
setBlobUrl(null);
};
}, [htmlContent]);
if (!blobUrl) {
return (
<div
role="status"
aria-label="Map loading"
style={{ width, height, display: "flex", alignItems: "center", justifyContent: "center" }}
>
Loading map…
</div>
);
}
return (
<iframe
ref={iframeRef}
src={blobUrl}
title="Embedded Folium Map"
width={width}
height={height}
style={{ border: "1px solid #e2e8f0", borderRadius: "6px", display: "block" }}
// allow-scripts — required for Leaflet JS initialisation
// allow-same-origin — required for tile XHR requests from blob: origin
// allow-forms — required for coordinate-picker popups
// Do NOT add allow-top-navigation or allow-modals unless you have an
// explicit requirement — they defeat the security boundary.
sandbox="allow-scripts allow-same-origin allow-forms"
referrerPolicy="no-referrer"
loading="lazy"
/>
);
};
export default FoliumMapEmbed;
Call it by passing the raw HTML string from your API response or state:
// Example: HTML string fetched from a Python/FastAPI endpoint
const [mapHtml, setMapHtml] = useState(null);
useEffect(() => {
fetch("/api/generate-map")
.then((r) => r.text())
.then(setMapHtml);
}, []);
return mapHtml ? <FoliumMapEmbed htmlContent={mapHtml} height="500px" /> : null;
Alternative Variants
srcdoc for Lightweight Prototypes
srcdoc inlines the HTML as an attribute value rather than a separate URL:
<iframe
srcdoc={htmlContent}
title="Prototype Folium Map"
sandbox="allow-scripts allow-same-origin"
width="100%"
height="400px"
/>
Use this only for prototypes. Production constraints that disqualify srcdoc:
| Constraint | Detail |
|---|---|
| Size limit | Browsers cap srcdoc payloads near 2 MB; Folium maps with embedded GeoJSON often exceed this |
| CSP context | srcdoc executes in the parent document’s security context, conflicting with strict Content-Security-Policy headers |
| Debugging | Network waterfall in DevTools hides tile requests — tile failures are invisible until the map is blank |
Sandbox Directive Reference
| Directive | Required for Folium | Risk if Added Without Need |
|---|---|---|
allow-scripts |
Yes — Leaflet JS will not run without it | Low; scripts are already sandboxed |
allow-same-origin |
Yes — tile XHR requests fail from blob: origin without it |
Medium; grants same-origin storage access |
allow-forms |
Only if map has coordinate-picker popups | Low |
allow-popups |
Only if external tile documentation links are needed | Medium; opens new browsing contexts |
allow-top-navigation |
Never for maps | High; allows iframe to redirect the top frame |
allow-modals |
Never for maps | Medium; allows alert() / confirm() dialogs |
Verification Steps
After wiring up FoliumMapEmbed, confirm correctness with these checks:
- Tiles render: Open the map and confirm background tiles load. If blank, check browser DevTools Network tab for blocked XHR requests — most commonly a missing
allow-same-origindirective. - No console errors in the parent frame: Open DevTools Console and confirm there are no
Refused to execute scriptorBlocked a framemessages. These indicatesandboxis too restrictive. - No parent CSS bleed: Inspect the parent app with DevTools and confirm Leaflet’s
.leaflet-containerrules do not appear in the parent document’s computed styles. - Blob URL revoked on unmount: In a React dev-tools profiler session, unmount the component and confirm the Blob URL no longer appears in
chrome://blob-internals/(Chrome only). If it does, the cleanupreturninuseEffectis not running. - Resize correctness: Resize the browser window and confirm tiles repaint without gaps. If tiles misalign, add a
ResizeObserveron the iframe container and calliframeRef.current.contentWindow.dispatchEvent(new Event("resize"))when dimensions change.
Common Errors and Fixes
Refused to execute inline script because it violates the following Content Security Policy directive
Root cause: Your host application has a strict Content-Security-Policy header with no blob: allowance in script-src. The Blob URL origin does not match 'self'.
Fix: Blob URL iframes execute in their own origin context — your server’s CSP header should not need modification. If you see this error it usually means the Folium HTML was injected via dangerouslySetInnerHTML rather than via an <iframe src={blobUrl}>. Confirm the iframe pattern is in use.
Blank iframe — no tiles, no map controls
Root cause: Missing allow-same-origin in the sandbox attribute. Tile requests from a blob: origin fail CORS preflight because the origin is opaque.
Fix: Add allow-same-origin to the sandbox value:
sandbox="allow-scripts allow-same-origin allow-forms"
URL.createObjectURL is not a function in SSR (Next.js / Remix)
Root cause: URL.createObjectURL is a browser-only API. Server-side rendering runs in Node.js where it does not exist.
Fix: Guard the effect with an environment check or move the Blob creation into a useEffect (which already only runs in the browser). The component above already does this correctly. If you see this error, verify no code paths call createObjectURL outside a useEffect.
Memory grows steadily in dashboards with frequent map updates
Root cause: URL.revokeObjectURL is not being called, or is called with a stale URL reference. Each re-render creates a new Blob and a new object URL that never releases.
Fix: The useEffect cleanup function in the production implementation above handles this. Verify the dependency array contains [htmlContent] — not [] (which skips cleanup on prop changes) and not no array (which re-runs on every render and may revoke too early).
Related
- Iframe Embedding & Isolation — parent cluster covering the full isolation architecture, CSP configuration, and cross-frame
postMessagepatterns - Making Python-Generated Maps Responsive on Mobile — companion technique for sizing iframe containers fluidly across viewports
- Exporting PyDeck Visualizations to Standalone HTML — same isolation pattern applied to PyDeck’s HTML output instead of Folium
- Static vs Dynamic Export Methods — choosing between pre-built HTML artifacts and server-rendered map APIs