javascriptreactjsnext.jsflowplayer

Correct way to cancel eventListeners with nextjs, react and flowplayer


I am currently using TypeScript in a nextJS project. I am using a cdn version of flowplayer, added to the page via a hook.

I have a variable on global scope of the component: video.

I am using useScript hook to load it. - https://usehooks.com/useScript/

 const status = useScript(
   '//cdn.flowplayer.com/players/433ff492-dbea-4527-9f9f-6dd08bc3b0be/native/flowplayer.async.js',
 );

When status becomes ready, this effect is activated:

useEffect(() => {
function renderFlowPlayer() {
  window.flowplayer?.cloud?.then(() => {
    video = window.flowplayer('#flowplayer');
    video.on('pause playing timeupdate ended', stateHandler);
    console.log(video);
  });
}
renderFlowPlayer();
return () => {
  if (video) {
    video.off('pause', stateHandler);
    video.off('playing', stateHandler);
    video.off('timeupdate', stateHandler);
    video.off('ended', stateHandler);
    video.destroy();
  }
 };
}, [status === 'ready]);

flowplayer.cloud.then is added to the window object when the script is ready.

This is my stateHandler function:

function stateHandler(ev) {
if (ev.type === 'pause') { console.log('paused'); }
if (ev.type === 'playing') { console.log(`Playing at ${event.target.currentTime}, duration is: 
${event.target.duration}, video ID is: ${event.target.opts.metadata.id}`); }
if (ev.type === 'timeupdate') { console.log(event.target.currentTime); }
if (ev.type === 'ended') { console.log('The end'); }

All console.log calls print the values correctly at first, however if I go to another page and come back, everything is printed duplicated. I guess these eventListeners aren't being killed correctly. What is the correct way to remove all these listeners when the component is about to unmount?

Thanks in advance!

The full code of the component is below:

import React, { useState, useEffect } from 'react';
import useScript from '@components/customHooks/useScript';

const SOURCES = ['b8302bc5-0f5f-4de6-a56e-6c05a9cd6025', '03513ba8-ea10-4c41-8f0b-82ab134b995c'];

let video = null;

export default function FlowPlayerWrapper() {
  const [demoSrc, setDemoSrc] = useState(SOURCES[0]);

  const status = useScript(
    '//cdn.flowplayer.com/players/433ff492-dbea-4527-9f9f-6dd08bc3b0be/native/flowplayer.async.js',
  );

  function stateHandler(ev : FlowPlayerEvent) {
    const Event = ev.target;
    if (ev.type === 'pause') { console.log('pausado'); }
    if (ev.type === 'playing') { console.log(`Tocando em ${Event.currentTime} e a duração é: ${Event.duration} e o id do vídeo: ${Event.opts.metadata.id}`); }
    if (ev.type === 'timeupdate') { console.log(Event.currentTime); }
    if (ev.type === 'ended') { console.log('Fim'); }
  }

  useEffect(() => {
    function renderFlowPlayer() {
      window.flowplayer?.cloud?.then(() => {
        video = window.flowplayer('#flowplayer');
        video.setSrc(demoSrc);
        video.on('pause playing timeupdate ended', stateHandler);
        console.log(video);
      });
    }
    renderFlowPlayer();
    return () => {
      if (video) {
        video.off('pause', stateHandler);
        video.off('playing', stateHandler);
        video.off('timeupdate', stateHandler);
        video.off('ended', stateHandler);
        video.destroy();
      }
    };
  }, [status === 'ready']);

  const togglePlay = () => {
    if (!video) return;
    video.togglePlay();
  };

  const toggleSrc = () => {
    if (!video) return;
    const nextIndex = SOURCES.indexOf(demoSrc) + 1;
    video.setSrc(SOURCES[nextIndex] || SOURCES[0]);
  };

  const toggleMute = () => {
    if (!video) return;
    video.toggleMute();
  };

  const toggleExtra = () => {
    if (!video) return;
    video.currentTime += 20;
  };

  const toggleFullScreen = () => {
    if (!video) return;
    video.toggleFullScreen();
  };

  return (
    <div>
      <div>
        Script status:
        {' '}
        <b>{status}</b>
      </div>
      <div id="flowplayer" data-player-id="afe735a8-5e21-4f91-9645-1ca2a6f4541d" />
      <div className="row">
        <div className="column">
          <h2>Chamadas API</h2>
          <button type="button" onClick={togglePlay}>Tocar / Pausar</button>
          <button type="button" onClick={toggleMute}>Mute / Unmute</button>
          <button type="button" onClick={toggleExtra}>Acrescentar 20s</button>
          <button type="button" onClick={toggleFullScreen}>Full screen</button>
          <button type="button" onClick={toggleSrc}>Mudar vídeo</button>
        </div>
      </div>
    </div>
  );
}


Solution

  • I've managed to fix it with this hook:

     useEffect(() => {
        function renderFlowPlayer() {
          // Função que está disponível após o script estar carregado e executando
          window.flowplayer?.cloud?.then(() => {
            video = window.flowplayer('#flowplayer');
            // id do video é settado na próxima linha
            video.setSrc(demoSrc);
            video.on('pause playing timeupdate ended', stateHandler);
          });
        }
        if (isInitialMount.current) {
          isInitialMount.current = false;
        } else {
          renderFlowPlayer();
        }
    
        return () => {
          if (video) {
            video.off('pause', stateHandler);
            video.off('playing', stateHandler);
            video.off('timeupdate', stateHandler);
            video.off('ended', stateHandler);
            video.destroy();
          }
         };
        }, [status === 'ready']);
    

    Making the effect run only when status is ready and not when the mounting phase finishes. https://reactjs.org/docs/hooks-faq.html#is-there-something-like-instance-variables