DEV Community

Cover image for Mastering the `<iframe>` Tag in React with TypeScript: A Comprehensive Guide
Serif COLAKEL
Serif COLAKEL

Posted on

Mastering the `<iframe>` Tag in React with TypeScript: A Comprehensive Guide

Embedding external content in a React application is a common requirement, whether it's a video, map, or third-party widget. The <iframe> HTML tag is a powerful tool for this, but it comes with nuances in React, especially regarding security, performance, and interactivity. This guide will walk you through all aspects of using <iframe> in React with TypeScript, complete with practical examples and best practices.

Code


Table of Contents

  1. Introduction to <iframe>
  2. Basic Usage in React
  3. Dynamic Control with State and Props
  4. Cross-Origin Communication with postMessage
  5. Security Best Practices
  6. Lazy Loading for Performance
  7. Displaying PDFs Without Toolbars
  8. Dynamic Resizing Strategies
  9. Accessibility Considerations
  10. Handling Errors and Edge Cases
    1. Detect Loading Failures
    2. CORS Issues
  11. Advanced Cross-Origin Communication with Two-Way Messaging
    1. Parent Component (ParentApp.tsx)
    2. Child Component (ChildApp.tsx)
  12. Conclusion

1. Introduction to <iframe>

An <iframe> (Inline Frame) embeds another HTML page within the current page. Common use cases include:

  • Embedding videos (YouTube, Vimeo)
  • Integrating maps (Google Maps)
  • Displaying third-party widgets (chat tools, calendars)
  • Rendering PDFs or documents

Key Challenges:

  • Security risks (XSS, Clickjacking)
  • Cross-origin communication
  • Performance optimization

2. Basic Usage in React

Start by rendering a static <iframe> with essential attributes.

Example: Embedding a Website

import React from "react";

const BasicIframe: React.FC = () => {
  return (
    <iframe
      src="https://www.example.com"
      width="600"
      height="400"
      title="Example Embed"
      allow="accelerometer; encrypted-media; gyroscope"
      referrerPolicy="strict-origin-when-cross-origin"
    />
  );
};

export default BasicIframe;
Enter fullscreen mode Exit fullscreen mode

Attributes Explained:

  • title: Mandatory for accessibility (WCAG 2.1).
  • allow: Grants specific permissions (e.g., camera access).
  • referrerPolicy: Controls how much referrer information is sent.

3. Dynamic Control with State and Props

Control <iframe> properties dynamically using React state.

Example: Switching URLs via Buttons

import React, { useState } from "react";

const DynamicIframe: React.FC = () => {
  const [url, setUrl] = useState("https://www.example.com");

  return (
    <div>
      <div>
        <button onClick={() => setUrl("https://www.google.com")}>Google</button>
        <button onClick={() => setUrl("https://www.wikipedia.org")}>
          Wikipedia
        </button>
      </div>
      <iframe src={url} width="600" height="400" title="Dynamic Content" />
    </div>
  );
};

export default DynamicIframe;
Enter fullscreen mode Exit fullscreen mode

Best Practices:

  • Always sanitize URLs to prevent XSS attacks.
  • Use useMemo for expensive computations if the URL depends on complex state.

4. Cross-Origin Communication with postMessage

Enable secure communication between the parent app and the embedded content.

Example: Sending and Receiving Messages

import React, { useEffect, useRef } from "react";

const IframeMessaging: React.FC = () => {
  const iframeRef = useRef<HTMLIFrameElement>(null);
  const trustedOrigin = "https://trusted-site.com";

  // Listen for messages from the iframe
  useEffect(() => {
    const handleMessage = (event: MessageEvent) => {
      if (event.origin !== trustedOrigin) return; // Validate origin
      console.log("Received:", event.data);
    };

    window.addEventListener("message", handleMessage);
    return () => window.removeEventListener("message", handleMessage);
  }, []);

  // Send a message to the iframe
  const sendMessage = () => {
    iframeRef.current?.contentWindow?.postMessage(
      { action: "SAVE_DATA", payload: "123" },
      trustedOrigin
    );
  };

  return (
    <div>
      <button onClick={sendMessage}>Send Data</button>
      <iframe
        ref={iframeRef}
        src={`${trustedOrigin}/embedded-app`}
        title="Secure Messaging Demo"
        width="600"
        height="400"
      />
    </div>
  );
};

export default IframeMessaging;
Enter fullscreen mode Exit fullscreen mode

Security Tips:

  • Always validate event.origin to ensure messages come from trusted sources.
  • Avoid using "*" as the target origin in postMessage.

5. Security Best Practices

Mitigate risks associated with embedding external content.

5.1 Restrict Permissions with sandbox

<iframe src="https://third-party.com" sandbox="allow-scripts allow-forms" />
Enter fullscreen mode Exit fullscreen mode
  • sandbox disables all features by default. Enable only what's necessary (MDN Reference).

5.2 Use X-Frame-Options

Ensure the embedded page sets the X-Frame-Options header to DENY or SAMEORIGIN to prevent Clickjacking.

5.3 Content Security Policy (CSP)

Add a CSP header to your React app to block unauthorized iframes:

Content-Security-Policy: frame-src 'self' https://trusted-site.com;
Enter fullscreen mode Exit fullscreen mode

6. Lazy Loading for Performance

Defer loading offscreen iframes to improve initial page load time.

Example: Lazy-Loaded Video Embed

const LazyVideo: React.FC = () => {
  return (
    <iframe
      src="https://www.youtube.com/embed/dQw4w9WgXcQ"
      title="Never Gonna Give You Up"
      width="600"
      height="400"
      loading="lazy"
    />
  );
};
Enter fullscreen mode Exit fullscreen mode

Browser Support:

  • Supported in Chrome, Firefox, and Edge. Use a polyfill or fallback for older browsers.

7. Displaying PDFs Without Toolbars

To hide toolbars in PDF viewers, append #toolbar=0 to the URL.

Example: PDF Viewer

interface PDFViewerProps {
  base64Data: string;
}

const PDFViewer: React.FC<PDFViewerProps> = ({ base64Data }) => {
  return (
    <iframe
      src={`data:application/pdf;base64,${base64Data}#toolbar=0`}
      width="100%"
      height="500px"
      title="Invoice PDF"
    />
  );
};
Enter fullscreen mode Exit fullscreen mode

Alternative Libraries:

  • Use @react-pdf/renderer for generating PDFs directly in React.

8. Dynamic Resizing Strategies

Adjust the iframe height based on its content.

Example: Auto-Resizing Iframe

import React, { useEffect, useRef } from "react";

const AutoHeightIframe: React.FC = () => {
  const iframeRef = useRef<HTMLIFrameElement>(null);

  useEffect(() => {
    const iframe = iframeRef.current;
    if (!iframe) return;

    const resizeObserver = new ResizeObserver((entries) => {
      iframe.style.height = `${entries[0].contentRect.height}px`;
    });

    iframe.contentWindow?.document.body &&
      resizeObserver.observe(iframe.contentWindow.document.body);

    return () => resizeObserver.disconnect();
  }, []);

  return (
    <iframe
      ref={iframeRef}
      src="/dynamic-content"
      width="100%"
      title="Auto-Resizing Iframe"
    />
  );
};
Enter fullscreen mode Exit fullscreen mode

Libraries:

  • Consider iframe-resizer-react for a production-ready solution.

9. Accessibility Considerations

  • Always include a title attribute to describe the iframe's purpose.
  • Provide a fallback link if the iframe fails to load:
  <iframe src="https://example.com" title="Accessible Iframe">
    <a href="https://example.com">View Content</a>
  </iframe>
Enter fullscreen mode Exit fullscreen mode

10. Handling Errors and Edge Cases

10.1 Detect Loading Failures

const ErrorHandlingIframe: React.FC = () => {
  const [hasError, setHasError] = useState(false);

  return (
    <>
      {hasError ? (
        <div>Failed to load content.</div>
      ) : (
        <iframe
          src="https://example.com"
          title="Error Demo"
          onError={() => setHasError(true)}
        />
      )}
    </>
  );
};
Enter fullscreen mode Exit fullscreen mode

10.2 CORS Issues

  • If the embedded content blocks cross-origin requests, use a proxy server or negotiate CORS headers with the content provider.

I'll integrate the provided example into the documentation as a new section. Here's the addition:


11. Advanced Cross-Origin Communication with Two-Way Messaging

This example demonstrates a complete parent-child iframe communication system with TypeScript, including error handling, loading states, and interval-based messaging.

Parent Component (ParentApp.tsx)

import React, { useCallback, useEffect, useRef, useState } from "react";

const childOrigin = "http://localhost:3002";

function ParentApp(): JSX.Element {
  const iframeRef = useRef<HTMLIFrameElement>(null);
  const [hasInterval, setHasInterval] = useState(false);
  const [errorEvent, setErrorEvent] = useState<string | null>(null);
  const [intervalTime, setIntervalTime] = useState(1000);

  const sendMessageToChild = useCallback((): boolean => {
    try {
      const data = {
        secretKey: crypto.randomUUID(),
        user: {
          name: "Serif Colakel",
          role: "ADMIN",
          age: Math.floor(Math.random() * 100),
        },
        receivedAt: new Date().toISOString(),
        intervalTime,
      };

      iframeRef.current?.contentWindow?.postMessage(data, childOrigin);
      return true;
    } catch (error) {
      setErrorEvent(
        `Failed to send data: ${error instanceof Error ? error.message : "Unknown error"}`
      );
      return false;
    }
  }, [intervalTime]);

  const handleInterval = useCallback(() => {
    if (hasInterval) {
      const intervalId = setInterval(sendMessageToChild, intervalTime);
      return () => clearInterval(intervalId);
    }
  }, [hasInterval, intervalTime, sendMessageToChild]);

  useEffect(handleInterval, [handleInterval]);

  const handleIframeError = useCallback(
    (e: React.SyntheticEvent<HTMLIFrameElement>) => {
      setErrorEvent("Failed to load child application");
      console.error("Iframe loading error:", e);
    },
    []
  );

  return (
    <div style={{ padding: "1rem" }}>
      <h1>Parent Application</h1>
      <div className="controls">
        <button onClick={sendMessageToChild}>
          Send Single Message
          <SendIcon />
        </button>
        <button onClick={() => setHasInterval(!hasInterval)}>
          {hasInterval ? "Stop Interval" : "Start Interval"}
          {hasInterval && <SendingIcon />}
        </button>
        <input
          type="number"
          value={intervalTime}
          onChange={(e) =>
            setIntervalTime(Math.max(100, Number(e.target.value)))
          }
          disabled={hasInterval}
        />
      </div>

      {errorEvent && <div className="error-message">{errorEvent}</div>}

      <iframe
        ref={iframeRef}
        src={childOrigin}
        title="Secure Child Application"
        onError={handleIframeError}
        style={{
          border: "1px solid #ccc",
          borderRadius: "8px",
          width: "100%",
          minHeight: "400px",
        }}
      />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Child Component (ChildApp.tsx)

import React, { useEffect, useState } from "react";

function ChildApp(): JSX.Element {
  const [receivedData, setReceivedData] = useState<Record<
    string,
    unknown
  > | null>(null);
  const parentOrigin = "http://localhost:3001";

  useEffect(() => {
    const handleMessage = (event: MessageEvent) => {
      if (event.origin !== parentOrigin) return;

      setReceivedData({
        data: event.data,
        count: (prev) => (prev?.count || 0) + 1,
        lastReceived: new Date().toISOString(),
      });
    };

    window.addEventListener("message", handleMessage);
    return () => window.removeEventListener("message", handleMessage);
  }, []);

  return (
    <div className="child-container">
      <h2>Child Application</h2>
      {receivedData ? (
        <div className="data-display">
          <pre>{JSON.stringify(receivedData, null, 2)}</pre>
        </div>
      ) : (
        <div className="loading-state">
          <svg>{/* Loading spinner */}</svg>
          <p>Awaiting initial message...</p>
        </div>
      )}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Key Features Explained:

  1. Secure Communication:
  • Origin validation in both directions
  • UUID generation for message tracking
  • Error boundaries for iframe loading
  1. Performance Optimization:
  • Debounced interval updates
  • Cleanup functions for event listeners
  • Conditional rendering based on state
  1. User Experience:
  • Visual feedback with animated SVG icons
  • Error state handling
  • Loading states for iframe content
  1. Type Safety:
    • Strict type definitions for message payloads
    • Type checking for event handlers
    • Generic state management

Security Enhancements:

const handleMessage = (event: MessageEvent) => {
  if (event.origin !== trustedOrigin) {
    console.warn(`Untrusted origin: ${event.origin}`);
    return;
  }
};

const secretKey = crypto.randomUUID();

const handleIntervalChange = (value: string) => {
  const sanitizedValue = Math.max(100, Math.min(Number(value), 5000));
  setIntervalTime(sanitizedValue);
};
Enter fullscreen mode Exit fullscreen mode

Best Practices Demonstrated:

  • Separation of concerns between presentation and logic
  • Proper event listener cleanup
  • Accessibility-aware SVG implementation
  • Responsive design with viewport units
  • Input sanitization for numerical values
  • Error boundary pattern implementation

12. Conclusion

Using <iframe> in React requires balancing functionality with security and performance. Key takeaways:

  • Validate and sanitize all user inputs used in iframe attributes.
  • Restrict permissions via sandbox and allow.
  • Monitor performance with lazy loading and ResizeObserver.
  • Always test across browsers and devices.

By following these practices, you can safely embed third-party content while maintaining a smooth user experience.


Further Reading:

Happy coding! 🚀

Top comments (0)