reactjspopupjsxonbeforeunload

window.addEventListener("beforeunload", callback) nulls out hook state values when webpage refresh is used - need to close a pop-up window


Use window.addEventListener("beforeunload", xxx) within a function call with the intent of having programmatic functionality before the user refreshes the page or navigates away. In this case would check a hook state before action is taken, however the hook state is nulled before the code is executed.

EXAMPLE: Have a pop-up window created with the push of a button, the button changes its text and functionality based on the window being open or not (closes if it is open)... works just fine.

I'm sure it is probably not perfectly accurate code (feedback is welcome!)

But I've noticed when the refresh button is hit, the window is kept open and React no longer controls its content (3D model rendering with React Three Fiber in the window). So the user would then have to hit the button and again create another window (as well as close the now useless window).

export function PopUp() {
  const [newWindow, setNewWindow] = useState(null);
  const [newRoot, setNewRoot] = useState(null);
  const containerRef = useRef(null);

  const openPopout = () => {
    // Check to see that the window isn't already open
    if (newWindow == null){
        const newWin = window.open('', '_blank', 'width=800,height=600');
        if (newWin) {
            
            // Create a container div in the new window's document
            const containerDiv = newWin.document.createElement('div');
            containerDiv.id = 'root';
            newWin.document.body.appendChild(containerDiv);
            containerRef.current = containerDiv;
            const newRooty = createRoot(newWin.document.getElementById("root"))
            setNewRoot(newRooty)
            setNewWindow(newWin);
        }
    }
    else{
      handleClose();
    }
  };

  const handleClose = () => {
      if (newRoot != null)
        newRoot.unmount();
      if (newWindow !=null)
        newWindow.close();
      setNewWindow(null);
      setNewRoot(null);
      containerRef.current = null;
      return "";
  };

  // Check for changes to newWindow
  useEffect(() => {
    if (newWindow && newRoot && containerRef.current) {
      newRoot.render(
        <>
        {/* <Scene/> */}
          <Canvas >
          <Box position={[1.2, 0, 0]} />
          </Canvas>
          </>
      )
      // this helps with the user closing the pop-up manually
      newWindow.addEventListener('beforeunload', handleClose);
      newWindow.addEventListener('handleError', (e) => {
        console.log('pop up window error')
        console.log(e)
      })
      return () => {
        newWindow.removeEventListener('beforeunload', handleClose);
      };
    }
  }, [newWindow]);

return (
    <>
      <button className="XL" onClick={openPopout}>
        { newWindow == null ? "Open Pop-up" : "Close Pop-Up"}
      </button>
    </>
  );

I added the below functionality to try and pre-empt the refresh action

  useEffect(() => {
    
    const unloadCallback = (event) => {
      if (newRoot != null){
        newRoot.unmount();
      }
      if (newWindow !=null)
        newWindow.close();

      setNewWindow(null);
      setNewRoot(null);
      containerRef.current = null;
      return "";
    };
  
    window.addEventListener("beforeunload", unloadCallback);
    return () => window.removeEventListener("beforeunload", unloadCallback);
  }, []);

So.... it doesn't work (the pop-up stays open), and one of the reasons is that newRoot and newWindow are both null by the time that code fires and are therefore the window/root are not closed/unmounted.... the If conditions (newRoot != null) and (newWindow != null) are false and their statements never fire. So for some reason the "beforeunload" is doing something before the callback is called.

I also tried

window.onbeforeunload = unloadCallback();

instead, but that seems to fire the code even later, like after the page has already re-loaded.

EDIT TO ADD: https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event#security

Looks like this is not a reliable functionality... Requires sticky activation - requires a user gesture or interaction (the intent is to have a pop-up dialog confirming "navigating away") and It is not reliably fired.


Solution

  • Because of the reasons mentioned in the question (EDIT TO ADD), from the documentation, this is not the intent of

    window.addEventListener("beforeunload",

    Answer is/was here:
    https://www.igvita.com/2015/11/20/dont-lose-user-and-app-state-use-page-visibility/

    Use this instead:

    // subscribe to visibility change events
    document.addEventListener('visibilitychange', function() {
      // fires when user switches tabs, apps, goes to homescreen, etc.
        if (document.visibilityState == 'hidden') { ... }
    
        // fires when app transitions from prerender, user returns to the app / tab.
        if (document.visibilityState == 'visible') { ... }
    });
    

    Downsides - this extends beyond hitting "refresh" or closing the page, it also fires when the user changes tabs (tab becomes hidden). So if there was a way to determine the reason the tab became hidden that would be great, but for now... so be it.