reflex icon indicating copy to clipboard operation
reflex copied to clipboard

on page unload event

Open moatboy opened this issue 1 year ago β€’ 4 comments

In react there is a beforeunload event which can be triggered when user closes browser window, how to do this in reflex? Went through reflex/page.py...would it be added here?

moatboy avatar Jan 26 '25 07:01 moatboy

I am actually trying to implement something similar today. Now, I'm going to advise against implementing something on the entire page, as it can cause performance issues (i.e. Firefox's bfcache won't cache pages if there is a beforeunload listener attached). It is also advised to use it sparingly, like when the user actually has unsaved changes like in an un-submitted form.

I created a MutationAwareState(rx.State) subclass, so I only implement it on states and vars that matter.

from typing import Any

import reflex as rx


class MutationAwareState(rx.State):
    """State class aware of base var mutations."""
    __user_modified_vars__: list[str] = []
    __before_unload_enabled__: bool = False

    @rx.event
    async def track_mutations(self):
        """Enable or disable the `beforeunload` listener accordingly."""
        if self.__user_modified_vars__ and not self.__before_unload_enabled__:
            yield rx.call_script('window.addEventListener("beforeunload", beforeUnloadHandler);')
            self.__before_unload_enabled__ = True
        if not self.__user_modified_vars__ and self.__before_unload_enabled__:
            yield rx.call_script('window.removeEventListener("beforeunload", beforeUnloadHandler);')
            self.__before_unload_enabled__ = False

    def __setattr__(self, name: str, value: Any):
        """Check our var and value, update tracking list accordingly, then set the var."""
        if name in self.base_vars:
            if name not in self.__user_modified_vars__ and name != rx.constants.ROUTER:
                self.__user_modified_vars__.append(name)
            if name in self.__user_modified_vars__ and self.get_fields()[name].default == value:
                self.__user_modified_vars__.remove(name)
        super().__setattr__(name, value)

You need to include the beforeUnloadHandler script, so add it to you app head components so it's available on every page:

app = rx.App(
    head_components=[
        rx.script(
            """
            const beforeUnloadHandler = (event) => {
                // Recommended
                event.preventDefault();

                // Included for legacy support, e.g. Chrome/Edge < 119
                event.returnValue = true;
            };"""
        ),
    ]
)

Now implement in your state that tracks your desired user input vars. I create a set_... for each var I want to track 1) for nicer IDE type hinting and static typing and 2) so I can add the track_mutations event (you can use other events, this is just what I use):

class MyFormState(MutationAwareState):
    username: str = ""
    email: str = ""
    password: str = ""  # There's no explicit setter, but this will still be tracked

    @rx.event
    async def set_username(self, value: str):
        self.username = value
        yield MyFormState.track_mutations

    @rx.event
    async def set_email(self, value: str):
        self.email = value
        yield MyFormState.track_mutations

There are a few caveats:

  • It only tracks base vars, not computed or backed. It DOES however include inherited base vars, so be careful with subclassing.
  • Even if you don't specifically implement a setter for a base var, its mutations are still tracked. However, only events that chain the track_mutations event will enable/disable the script.
  • If there are vars you DON'T want to be tracked, create them as backend vars and use computed for the front end renders.

riebecj avatar Apr 09 '25 20:04 riebecj

In an ideal scenario, much like how Pydantic uses their Field() class to set default, default_factory, etc, the Reflex team should configure their rx.field method to include some of those features that may be parsed by their states, like if it should trigger the beforeunload event if its value isn't a default. It's a tall ask though.

riebecj avatar Apr 09 '25 20:04 riebecj

In react there is a beforeunload event which can be triggered when user closes browser window, how to do this in reflex? Went through reflex/page.py...would it be added here?

I think the only way right now would be to spwan a background event which every n seconds would check if the current connection (session id) is still active. Not perfect but probably effective.

PasqualePuzio avatar Jun 10 '25 11:06 PasqualePuzio