Vimeo API with ReactJS and Plyr

2.7k views Asked by At

I have a React component that uses the https://github.com/selz/plyr media player. Everything works fine until the component unmounts, which results in an error from the Vimeo API. Specifically: Uncaught (in promise) TypeError: Cannot read property 'postMessage' of null.

After this error occurs, I try to load the module again it will fail due to this.player being undefined, but if you destroy that, and try again it will load. Perhaps the React Tree is saving the first iteration of the component, and I need to completely destroy it somehow?

Here's my component:

import React, {Component, PropTypes} from 'react';
import {findDOMNode} from 'react-dom';
import plyr from 'plyr';

/**
 * @desc Regex to retrieve the Vimeo video ID from the URL.
 * @type {RegExp}
 */
const regex = /^.*(vimeo\.com\/)((channels\/[A-z]+\/)|(groups\/[A-z]+\/videos\/))?([0-9]+)/g;

export default class Vimeo extends Component {

  static propTypes = {
    url: PropTypes.string.isRequired,
  };

  constructor(props) {
    super(props);
    // Use regex to get the video id from the url
    this.videoId = regex.exec(this.props.url);
  }

  /*
   |--------------------------------------------------------------------------
   | Digest Cycle
   |--------------------------------------------------------------------------
   */

  /**
   * @desc Initiate video player.
   */
  componentDidMount() {
    this.player = plyr.setup(findDOMNode(this), {
      controls: [],
      autoplay: true,
      loop: true,
      mute: true,
    });
    this.player[0].on('ready', () => {
      // Mute the video
      if (!this.player[0].isMuted()) {
        this.player[0].toggleMute();
      }
    });
  }

  /**
   * @desc Destroy video player
   */
  componentWillUnmount() {
    this.player[0].destroy();
  }

  /*
   |--------------------------------------------------------------------------
   | Render
   |--------------------------------------------------------------------------
   */

  render() {
    let player = null;
    if (typeof this.videoId !== 'undefined' && this.videoId !== null) {
      player = (
        <div>
          <div
            data-type="vimeo"
            data-video-id={this.videoId}
          />
        </div>
      );
    }

    return player;
  }
}
2

There are 2 answers

0
Genaro Rocha On

Your problem is being caused by the way in which the Plyr object gets destroyed. Take a look at the _destroy() method, which is what is exposed in the Plyr api.

https://github.com/Selz/plyr/blob/master/src/js/plyr.js#L3234

As you can see, in the case of Vimeo, this line plyr.embed.unload().then(cleanUp); and the following call to setTimeout make it clear that _destroy is not a synchronous call.

First, plyr.embed.unload() makes a call to this:

https://github.com/vimeo/player.js/blob/e0f607196f7b38e3a9891e70dda38da2731cff79/src/player.js#L440

which returns a Promise. Once the promise is fulfilled, the cleanup function in Plyr gets executed, and that is when the DOM gets updated and variables reset. What this means is that your component will unmount without waiting for the Plyr instance to be fully restored to default or "destroyed". This may cause unexpected behaviour if you re-mount the component right away.

Inside componentWillUnmount, I suggest that you do not rely on destroy() to update the DOM and reset the Plyr instance. You can instead delete the DOM element wrapping the video embed and just set this.player to null so the Plyr instance inside gets nuked and collected by the gc. The next time your component gets mounted, the DOM elements that were holding the video wont be there and your player will initialize normally, setting a new Plyr instance and creating the necessary html again.

0
Preview On

You can use the ReactTransitionGroup low-level API to get access to a new lifecycle, componentWillLeave.

npm i -S react-addons-transition-group

It will allow you to wait for the player to have been destroyed before un-mounting your component, by calling the provided callback. A timeout of 200ms should be enough, but you can increase it if you want to be safe.

So get rid of your componentWillUnmount and replace it by something like

componentWillLeave(cb) {
  this.player[0].destroy();
  setTimeout(cb, 200);
}

You'll have to wrap your Vimeo component into a ReactTransitionGroup, but you can of course create a wrapper if you don't like it and it will be reused some place else.


Also, you might want to look at react-plyr. It has the same issue as you and is quite new, but it has tests, nice structure and supports more options. Might be a good idea to join efforts to make it a cool library if you have time ;)