As part of a project I’m working on, I recently needed to open a new (non-modal) window to display some information to the user. If this was desktop development, which I’m generally more familiar with, this would be easy, but I didn’t think there was any way to do it in a single-page React app.

As it turns out, it’s actually very simple - as long as you’re using React v16 or higher.

React 16 introduced Portals. Whereas a typical React component renders its HTML as a child of its parent node, a component which uses a Portal allows you to render the HTML anywhere. You’d usually use this to create a floating tooltip or menu somewhere else in the DOM hierarchy, which is pretty clever in itself, but anywhere can also encompass a totally separate DOM - which is what we’re going to do here.

Let’s cut to the chase - here’s the source code for a component I’ve put together that you can add into your project more or less as-is. Quick caveat: I’m using typescript rather than plain javascript but I’m sure you can make any adjustments you need.

import React from 'react';
import ReactDOM from 'react-dom';

interface Props {
    title: string;                          // The title of the popout window
    closeWindow: () => void;                // Callback to close the popout
}

interface State {
    externalWindow: Window | null;          // The popout window
    containerElement: HTMLElement | null;   // The root element of the popout window
}

export default class Popout extends React.Component<Props, State> {
    constructor(props: Props) {
        super(props);
        this.state = {
            externalWindow: null,
            containerElement: null
        };
    }

    // When we create this component, open a new window
    public componentDidMount() {
        const features = 'width=800, height=500, left=300, top=200';
        const externalWindow = window.open('', '', features);

        let containerElement = null;
        if (externalWindow) {
            containerElement = externalWindow.document.createElement('div');
            externalWindow.document.body.appendChild(containerElement);

            // Copy the app's styles into the new window
            const stylesheets = Array.from(document.styleSheets);
            stylesheets.forEach(stylesheet => {
                const css = stylesheet as CSSStyleSheet;

                if (stylesheet.href) {
                    const newStyleElement = document.createElement('link');
                    newStyleElement.rel = 'stylesheet';
                    newStyleElement.href = stylesheet.href;
                    externalWindow.document.head.appendChild(newStyleElement);
                } else if (css && css.cssRules && css.cssRules.length > 0) {
                    const newStyleElement = document.createElement('style');
                    Array.from(css.cssRules).forEach(rule => {
                        newStyleElement.appendChild(document.createTextNode(rule.cssText));
                    });
                    externalWindow.document.head.appendChild(newStyleElement);
                }
            });

            externalWindow.document.title = this.props.title;

            // Make sure the window closes when the component unloads
            externalWindow.addEventListener('beforeunload', () => {
                this.props.closeWindow();
            });
        }

        this.setState({
            externalWindow: externalWindow,
            containerElement: containerElement
        });
    }

    // Make sure the window closes when the component unmounts
    public componentWillUnmount() {
        if (this.state.externalWindow) {
            this.state.externalWindow.close();
        }
    }

    public render() {
        if (!this.state.containerElement) {
            return null;
        }

        // Render this component's children into the root element of the popout window
        return ReactDOM.createPortal(this.props.children, this.state.containerElement);
    }
}

Your first question will probably be ‘How do I use this?’ I’ve got you covered. Here are the relevant bits of a component that uses the Popout component.

// We need a state field to govern whether the popout is shown or not
constructor() {
    this.state = {
        showPopout: false
    };
}

// This sets the above state variable
private setPopoutOpen(open: boolean) {
    this.setState({
        showPopout: open
    });
}

// When this component is unloaded, make sure we close the popout
public componentDidMount() {
    window.addEventListener('beforeunload', () => {
        this.setPopoutOpen(false);
    });
}

// This returns the HTML for the popout, or null if the popout isn't visible
private getPopout() {
    if (!this.state.showPopout) {
        return null;
    }

    return (
        <Popout title='Your Popout Title' closeWindow={() => this.setPopoutOpen(false)}>
            <div>YOUR POPOUT CONTENT HERE</div>
        </Popout>
    );
}

// Render the popout and a button to show / hide it
public render() {
    return (
        <div>
            {this.getPopout()}
            <button onClick={() => this.setPopoutOpen(!this.state.showPopout)}>
                toggle popout
            </button>
        </div>
    );
}

I imagine most of that is self-explanatory if you’ve worked with React before - it’s more or less just a state variable, a setter for it, and a render() method.

The two things I want to draw your attention to are:

  • The content for the popout window all lives in the component that controls the popout, and not in the Popout component itself.
  • Leading on from this, the owner component and the popout window share the same state. This is huge - you don’t need to do any work to sync anything.

It’s at about this point that the word ‘popout’ no longer looks like a real word to me.

Hopefully that’s been useful!