How to save data to database when the user request to leave?

209 Views Asked by At

What is the proper way to do that? My goal is to save in database the count of minutes of the video the user has watched until he closed the page and set the video's minutes to where he left off, next time he opens the very same video. I've tried to do that with beforeunload event but the page is closing before the function return. I'm using async/await but I don't know how to make sure the event finish to run properly. I've tried something like this in my react component:

const ec = async (e) => await handleBeforeUnload(e);
    window.addEventListener('beforeunload', ec);

    return () => {
        window.removeEventListener('beforeunload', ec);
    }

but doesn't gurante that the function finish before leaving the page. What is the proper way to do that?

edit: here's the rest of the code:

    const handleBeforeUnload = async (event) => {
        await save_timestamp();
    }

    const save_timestamp = async () => {

    const v:IResumeVideo = {
        seconds: timeline.current,
        video_id: video_id
    }

    const response = await fetch('/api/save_video', {
        method: 'POST',
        body: JSON.stringify(v)
      });
    const result = await response.json();           
}
4

There are 4 best solutions below

3
Evren On BEST ANSWER

You can create a dedicated hook with sendBeacon as Elvis mentioned in the comment, in this way you can use also for different cases in different components without duplicating same code

const useUnloadBeacon = ({ url, data }) => {
  const handleUnload = async () => {
    const jsonData = JSON.stringify(data);

    const sendBeaconSuccess = navigator.sendBeacon(url, jsonData);
    if(!sendBeaconSuccess) {
     try {
       const response = await fetch(url, {
         method: 'POST',
         body: jsonData,
         headers: {
           'Content-Type': 'application/json',
         },
       });
 
       const result = await response.json();
     } catch (error) {
       console.error(error);
     }
    }
  };

  useEffect(() => {
    window.addEventListener('unload', handleUnload);

    return () => {
      window.removeEventListener('unload', handleUnload);
    };
  }, [url, data]);
};

export default useUnloadBeacon;
1
mimikyu On

It seems that using beforeunload is not recommended at all for your using case of saving the user's state, see: https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event:

The beforeunload event suffers from some problems: It is not reliably fired, especially on mobile platforms. For example, the beforeunload event is not fired at all in the following scenario: A mobile user visits your page. The user then switches to a different app. Later, the user closes the browser from the app manager.

Note: It is recommended to use the visibilitychange event as a more reliable signal for automatic app state saving that gets around problems like the above. See Don't lose user and app state, use Page Visibility for more details.

EDIT: I've briefly tested in a fiddle what happens if you have a async function on the visibilitychange, and it seems to be reliably fired. The code snippet in question is:

document.addEventListener('visibilitychange', async () => {
    if (document.visibilityState === 'hidden') {
        await timeout(1000); // this would be your async api call
        console.log('was hidden!');
    }
});

function timeout(s) {
    return new Promise(r => {
    setTimeout(r, s);
  });
}
0
Eduardo Poço On

There is a web API, the Beacon API, to do exactly that and guarantee the data is sent even after the user leaves the page:

https://developer.mozilla.org/en-US/docs/Web/API/Beacon_API

document.addEventListener("visibilitychange", function logData() {
    if (document.visibilityState === "hidden") {
        navigator.sendBeacon("/log", analyticsData);
    }
});
0
passingThru On

Another contender for catching a user "leaving" a tab/window, is onmouseout...

Win_ID.contentWindow.addEventListener('mouseout',function(e){

},true,options);

Another format...

Win_ID.contentWindow.onmouseout=function(e){

};

You're just gonna have to run tests on different devices to see which method is best supported and suits your needs.