DEV Community

loading...

Using HTML Frames in React

Edwin
Excited about React, web dev in general, couch coop games and anything that involves loud guitars! 🤘
・5 min read

Yes, you read that correctly: <frame>.

You know, that funny HTML tag that disappeared from the face of the internet at about the same time as <marquee>, GeoCities, guest book pages, animated "UNDER CONSTRUCTION" gifs and the rest of Web 1.0.

I wanted to see if it is possible to render a React component to a HTML <frame>. Here is the end result:

Frames 101

The only thing that's harder to find than documentation on frames is toilet paper at the start of a pandemic. So here's a quick refresher on how they work:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN"
   "http://www.w3.org/TR/html4/frameset.dtd">
<HTML>
  <HEAD>
    <TITLE>A simple frameset document</TITLE>
  </HEAD>
  <FRAMESET cols="20%, 80%">
    <FRAME name="menu" src="menu.html">
    <FRAME name="content" src="content.html">
  </FRAMESET>
</HTML>

Frames need to be inside a <frameset>, which replaces the <body> tag and defines how the frames are divided into rows and/or columns. The rows and cols attributes can take percentage values, absolute numbers (as pixels) or an asterisk to tell it to take the remainder of the space. Framesets can be nested inside each other, in order to create a layout with multiple dividers.

Using frames and framesets, you can create layouts where parts of the page remain visible (such as the header and navigation menu), while the main content of the website is replaced.

A frame has a name attribute, that can be used in hyperlinks to replace the content of that frame:

<a href="page2.html" target="content">Go to page 3</a>

Where did they go?

I remember building websites using frames years ago and I was wondering what happened to them. The short answer is that the <frame> element was introduced in HTML version 4.0 (1999) and deprecated in HTML5 (2014).

Frames break a fundamental principle of the web, namely that a page is a single unit of information that can be viewed and clicked/navigated to using a URL. The information that is shown inside frames, however, depends on a sequence of navigation actions by the user.

Perhaps the same thing can be said about modern Single Page Applications with their dynamic content in modal screens, sidebars and infinite scrolling lists, but that's a discussion for a different time.

It's the year 2020, which means you should use CSS to lay out the information on your webpage. Now that flexbox is supported by all major browsers, it is super easy to create these type of layouts with a bit of CSS and a few semantic HTML5 elements.

What Mozilla says about frames:

Deprecated
This feature is no longer recommended. Though some browsers might still support it, it may have already been removed from the relevant web standards, may be in the process of being dropped, or may only be kept for compatibility purposes. Avoid using it, and update existing code if possible; see the compatibility table at the bottom of this page to guide your decision. Be aware that this feature may cease to work at any time.

Just because the internet says we should not use frames, does not mean it is forbidden to do so, right?

Using frames with React

I got inspired by the react-frame-component library that allows you to render a React app inside an <iframe> and I was curious if I could do the same thing for <frame>.

I wanted to create something similar to the react-frame-component library, where you can pass in any React component to a <Frame> component, which renders it inside its document. These components are abstractions that resemble the original frame and frameset elements:

<Frameset rows="20%, 80%">
  <Frame name="nav">
    <Menu />
  </Frame>
  <Frame name="main">
    <Home />
  </Frame>
</Frameset>

Instead of a src attribute of the frame that points to a page, you pass the child component that you want to render. It should also accept the original frame attributes, such as options to disable the frame border and (dis)allow resizing. The name attribute is used to target the frame for navigation purposes (see below).

Frameset

The <frameset> replaces the <body> tag, which is kind of an issue in React, since rendering a React app requires a DOM element container to render the app into. In the end, I decided to let the <html> be the root of the React app, which means the app won't have a <head> anymore (no styling, no title).

I could not pass the cols and rows to the <frameset> element as props using React, so I have to use a ref and set the attributes using JS. I still don't know why.

class Frameset extends React.Component {
    constructor(props) {
        super(props);
        this.ref = React.createRef();
    }
    componentDidMount() {
        if (this.props.cols) {
            this.ref.current.setAttribute("cols", this.props.cols);
        }
        if (this.props.rows) {
            this.ref.current.setAttribute("rows", this.props.rows);
        }
    }
    render() {
        return (
            <frameset ref={this.ref}>{this.props.children}</frameset>
        );
    }
}

React will also show us a nasty warning that we cannot nest frameset elements, which I believe is incorrect:

Warning: validateDOMNesting(...): <frameset> cannot appear as a child of <frameset>.
    in frameset (at Frameset.js:45)

Frame

Implementing the frame itself is a bit more complex. We need to access the frame's document and render the React component to it.

The document can easily be accessed using the contentDocument property.

The react-frame-component library used document.write() to overwrite the content of the document. This works, but it is not recommended by modern browsers:

let doc = this.frameRef.current.contentDocument;
doc.open("text/html", "replace");
doc.write(
    '<!DOCTYPE html><html><head></head><body style="margin:0"><div id="root"></div></body></html>'
);
doc.close();

I tried simply rendering a new React component tree to the body of the contentDocument, but then the page briefly displayed the content and disappeared again, resulting in an empty page. Waiting for the onLoad event of the frame is not possible using React, but a delayed render using setTimeout() seemed to do the trick.

React again complained, this time that we should not render the app directly to the document body, so I manually inserted a container element. Then, we can render our component to this container on every componentDidUpdate():

ReactDOM.render(this.props.children, this.rootRef.current);

Replacing a frame's content

I ended up with a component that renders a component to a target frame, similar to the <a target="content"> in HTML:

<Link target="main" component={Home}>Go Home</Link>

To make this work across component trees, I used a React Context inside each <Frameset> component, that keeps track of which component is rendered inside each frame by it's name and a function to replace the component of a target frame.

Styling

The fact that every frame has its own document completely breaks importing styles in components that are rendered inside a frame. I came up with a custom useStyle() hook that adds a stylesheet from the public assets directory to the frame's document. Not pretty, but good enough for this project.

End result

The basic functionality of HTML frames was successfully implemented. It is possible to nest framesets and create a layout with multiple dividers. The implementation required some hacks and I really doubt sharing state across components works. I have not even tested it in production mode, but it was never meant to be used in production anyway.

The full app is here:

Discussion (0)