diff --git a/src/players/YouTube.js b/src/players/YouTube.js index 8498c617..f6f66398 100644 --- a/src/players/YouTube.js +++ b/src/players/YouTube.js @@ -20,6 +20,21 @@ export default class YouTube extends Component { this.props.onMount && this.props.onMount(this) } + componentWillUnmount () { + // The YouTube iframe API keeps a reference to every player it creates, so + // removing the container from the DOM is not enough to release the iframe. + // Without destroy() the iframe stays detached but reachable from window.YT, + // leaking the player and everything its event handlers close over. + if (this.player && typeof this.player.destroy === 'function') { + try { + this.player.destroy() + } catch (error) { + // destroy() throws if the iframe has already been removed + } + } + this.player = null + } + getID (url) { if (!url || url instanceof Array || MATCH_PLAYLIST.test(url)) { return null diff --git a/test/players/YouTube.js b/test/players/YouTube.js index f2c8c30f..90276b8b 100644 --- a/test/players/YouTube.js +++ b/test/players/YouTube.js @@ -78,6 +78,32 @@ test('load() when ready', t => { getSDK.restore() }) +test('componentWillUnmount() destroys the player', t => { + const destroy = sinon.fake() + const renderer = create() + const instance = renderer.getInstance() + instance.player = { destroy } + renderer.unmount() + t.ok(destroy.calledOnce, 'destroy() is called on unmount') + t.ok(instance.player === null, 'the player reference is released') +}) + +test('componentWillUnmount() without a player', t => { + const renderer = create() + renderer.unmount() + t.ok(true, 'unmounting before the player is ready does not throw') +}) + +test('componentWillUnmount() when destroy() throws', t => { + const renderer = create() + const instance = renderer.getInstance() + instance.player = { + destroy: () => { throw new Error('The YouTube player is not attached to the DOM') } + } + renderer.unmount() + t.ok(instance.player === null, 'a throwing destroy() does not break unmounting') +}) + test('onStateChange() - play', t => { const called = {} const onPlay = () => { called.onPlay = true }