Using a React 16 Portal to do something cool

React 16 is here, and one of the more interesting additions is ‘Portals’.
Portals let you render a bit of React-controlled DOM outside of the parent component. The React docs explain it nicely, using the example of a modal. It works well for tooltips, too (here’s one I made earlier).
But none of that is very interesting. Let’s do something weird…
Since all a portal does is take an element and append it to some other element, you aren’t limited to sticking it elsewhere in the current document. You can append it to the body in a different document, perhaps a document in an entirely different window.
Below I have a base page (on ze left) that has a counter and a crimson button, and a window (on the right) that’s part of the same React app, and also a forest (in the background).
The fact that the window on the right is the same React app is what you’re supposed to be excited about.
You can to take my word for it that the numbers go up in unison, or click
Everything you see in the picture above (except the trees) is in the one component below.
class App extends React.PureComponent {
  constructor(props) {
    super(props);
    
    this.state = {
      counter: 0,
      showWindowPortal: false,
    };
    
    this.toggleWindowPortal = this.toggleWindowPortal.bind(this);
  }

  componentDidMount() {
    window.setInterval(() => {
      this.setState(state => ({
        ...state,
        counter: state.counter + 1,
      }));
    }, 1000);
  }
  
  toggleWindowPortal() {
    this.setState(state => ({
      ...state,
      showWindowPortal: !state.showWindowPortal,
    }));
  }
  
  render() {
    return (
      <div>
        <h1>Counter: {this.state.counter}</h1>
        
        <button onClick={this.toggleWindowPortal}>
          {this.state.showWindowPortal ? 'Close the' : 'Open a'} Portal
        </button>
        
        {this.state.showWindowPortal && (
          <MyWindowPortal>
            <h1>Counter in a portal: {this.state.counter}</h1>
            <p>Even though I render in a different window, I share state!</p>
            
            <button onClick={() => this.setState({ showWindowPortal: false })} >
              Close me!
            </button>
          </MyWindowPortal>
        )}
      </div>
    );
  }
}
You’ve worked out by now that
<MyWindowPortal>
is a little bit special, and anything inside it is going to get rendered in a different window.
You are correct, and I am proud of you. Specifically,
<MyWindowPortal>
does two things:
Opens a new browser window when it mountsCreates a ‘portal’ and appends
props.children
to the body of the new window
Isn’t that the coolest thing?
I’m so excited I have to go for a walk.
I saw a duck!
Below is the body of the component from above. The part that’s new in React 16 is
ReactDOM.createPortal
on line 11 — that’s where the magic happens.
class MyWindowPortal extends React.PureComponent {
  constructor(props) {
    super(props);
    // STEP 1: create a container <div>
    this.containerEl = document.createElement('div');
    this.externalWindow = null;
  }
  
  render() {
    // STEP 2: append props.children to the container <div> that isn't mounted anywhere yet
    return ReactDOM.createPortal(this.props.children, this.containerEl);
  }

  componentDidMount() {
    // STEP 3: open a new browser window and store a reference to it
    this.externalWindow = window.open('', '', 'width=600,height=400,left=200,top=200');

    // STEP 4: append the container <div> (that has props.children appended to it) to the body of the new window
    this.externalWindow.document.body.appendChild(this.containerEl);
  }

  componentWillUnmount() {
    // STEP 5: This will fire when this.state.showWindowPortal in the parent component becomes false
    // So we tidy up by closing the window
    this.externalWindow.close();
  }
}
I apologise for putting the lifecycle methods in a weird order
Does that make sense? The component doesn’t return something, it does something elsewhere.
Maybe another way to think about it is this: normally, a parent component says to a child component: “hey, render some DOM, then append the results to me”, and the child component does what it’s told. But in this case, the obstreperous child says “No! I’m gonna render stuff in a different window and write a blog post about it!”
Now, I know what you’re thinking.
You’re thirsty and you’re wondering if you should have some water. Yes, go have a drink.
The other thing you’re probably thinking is: what good is it being able to inject some DOM into a blank window if it’s unstyled? Maybe if it’s Craigslist or Wikipedia no one will notice, but your site is beautiful, you can’t have your little chat window pop out thing being all times-new-romany.
Well, good news, everybody!
At first I hoped there would be an easy way to copy the styles into the new window. Then I remembered that my life is little more than a series of meaningless tasks to fill the minutes and hours, their only purpose to keep me distracted from the deep, howling emptiness inside.
So writing the function myself was fun!
Here it is:
function copyStyles(sourceDoc, targetDoc) {
  Array.from(sourceDoc.styleSheets).forEach(styleSheet => {
    if (styleSheet.cssRules) { // for <style> elements
      const newStyleEl = sourceDoc.createElement('style');

      Array.from(styleSheet.cssRules).forEach(cssRule => {
        // write the text of each rule into the body of the style element
        newStyleEl.appendChild(sourceDoc.createTextNode(cssRule.cssText));
      });

      targetDoc.head.appendChild(newStyleEl);
    } else if (styleSheet.href) { // for <link> elements loading CSS from a URL
      const newLinkEl = sourceDoc.createElement('link');

      newLinkEl.rel = 'stylesheet';
      newLinkEl.href = styleSheet.href;
      targetDoc.head.appendChild(newLinkEl);
    }
  });
}
All this
styleSheet
business is not stuff that I actually know about, so I look forward to being told the smart way to do this in the comments.
Now I can copy the styles across just after opening the new window, like so:
this.externalWindow = window.open(/* ... */);
                                  
copyStyles(document, this.externalWindow.document);
Here it all is in a codepen:
https://codepen.io/davidgilbertson/pen/xPVMqp
I don’t think it will work from within the Medium app on iOS/Android, so hit the codepen icon in the top right to open it in a browser.
I think this is the end of the blog post.
Bye, everybody!

Tags

More by David Gilbertson

React
Javascript
Javascript
Javascript
Javascript
Trading
React
Hackernoon Top Story
Hackernoon Top Story
Javascript
Blockchain
Blockchain
Bitcoin
Blockchain
Bitcoin
Bitcoin
Bitcoin
Bitcoin
Ipfs
Blockchain
Bitcoin
Blockchain
Bitcoin
Bitcoin
Trading
Trading
Trading
Bitcoin
Security
Javascript
Javascript
Javascript
Javascript
Recruiting
Javascript
Javascript
Github
Accessibility
Javascript
Web Development
Git
Javascript
Typography
Css
Machine Learning
Css
React
Css
Web Development
Javascript
Javascript
Svg
React
Javascript
React
Nodejs
Javascript
Web Development
Javascript
Git
Testing
Css
Javascript
Javascript
Design
Javascript
Javascript
Topics of interest