Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

State class variable is not shareing across pages and tabs #1857

Closed
nssnaresh5 opened this issue Sep 24, 2023 · 1 comment
Closed

State class variable is not shareing across pages and tabs #1857

nssnaresh5 opened this issue Sep 24, 2023 · 1 comment

Comments

@nssnaresh5
Copy link

Describe the bug
The state variable is not shared between the pages.

To Reproduce
The string variable is declared as a string in the state class.

  • Code/Link to Repo:
    code attached here as a python file code.txt

Expected behavior

  • The class variable should be shared across the pages and tabs.

Screenshots
image

Specifics (please complete the following information):

  • Python Version: Python 3.11.3
  • Reflex Version: 0.2.8
  • OS: Microsoft Windows 10 Pro
  • Browser (Optional): Google Chrome

Additional context
Your effort on this library is literally awesome. I'm a big fan of your Reflex library.
but Your examples in the main documentation of the Reflex web page are not sufficient. If you provide more examples with different use cases for each component would be greatly helpful.
code.txt

@masenf
Copy link
Collaborator

masenf commented Sep 24, 2023

In reflex, each tab gets its own state. So it's "expected" that opening the app in two different tabs will result in each tab storing colorState independently.

However, within a single tab when I navigate between page1 and page2, I do see that the state is consistent; going to page1 and clicking the button makes it red, then on page2 I still see red.

There is not a trivial way to link 2 states together... however you may consider the use of rx.LocalStorage or rx.Cookie which leverages the client browser to syncronize values across tabs.

Changing the colorState definition to colorState: str = rx.Cookie("green") gets a little closer to the behavior you want... however it still doesn't live update across tabs; but opening a new tab retains the value set on a previous tab and changing the value on the new tab then refreshing the previous tab will show the value from the new tab.

There is not currently a simple mechanism in reflex to keep the state of two or more tabs in sync live, because the reflex backend doesn't really know that any two states belong to the same user.

If this is something that you want, then you can

  1. set a cookie in the client browser
  2. associate all "active" sessions with that cookie on initial page load
  3. use app.modify_state to explicitly update all associated states when any one state changes.
  4. track disconnects and update session tracking dicts accordingly

I should stress that this is kind of a hack, but it does at least use public APIs.

Here is a bit of sample code to help if you want to go down this road:

import reflex as rx

# Keep track of tokens associated with the same client browser ("shared" sessions)
shared_sessions_by_token: dict[str, set[str]] = {}

# Keep track of (shared_token, state_token) pairs for each websocket connection (sid)
tokens_by_sid: dict[str, tuple[str, str]] = {}


class State(rx.State):
    # The clientToken is saved in the browser and identifies "shared" sessions
    clientToken: str = rx.Cookie("")

    # The colorState is a special variable that is shared among all sessions with the same clientToken
    colorState: str = "green"

    def ChangePage1Color(self):
        self.colorState = "red"

        # Apply changes to all other shared sessions
        return State.set_color_state_for_shared_sessions

    def ChangePage2Color(self):
        self.colorState = "yellow"

        # Apply changes to all other shared sessions
        return State.set_color_state_for_shared_sessions

    async def set_color_state_for_shared_sessions(self):
        """Iterate through all shared sessions and update them with the new colorState."""
        if not self.clientToken:
            self.set_client_token()

        print(f"{self.clientToken} -> {shared_sessions_by_token[self.clientToken]}")

        for token in shared_sessions_by_token.get(self.clientToken, set()):
            if token != self.get_token():
                async with app.modify_state(token) as other_state:
                    other_state.colorState = self.colorState

    async def set_color_state_for_new_session(self):
        """When a new session is created, copy the colorState from another shared session."""
        for token in shared_sessions_by_token.get(self.clientToken, set()):
            if token != self.get_token():
                async with app.modify_state(token) as other_state:
                    self.colorState = other_state.colorState
                    return

    def set_client_token(self):
        """Page on_load handler uses the clientToken cookie to identify shared sessions."""
        if not self.clientToken:
            self.clientToken = self.get_token()

        # Mark this state's token as belonging to the clientToken
        shared_sessions_by_token.setdefault(self.clientToken, set()).add(self.get_token())

        # Mark this state's websocket id as belonging to the clientToken and state token
        tokens_by_sid[self.get_sid()] = (self.clientToken, self.get_token())

        # Set the colorState for the new session from existing shared sessions (if any)
        return State.set_color_state_for_new_session


def page1() -> rx.Component:
    return rx.vstack(
        rx.button(
            "Click 1",
            on_click=State.ChangePage1Color,
        ),
        rx.text(
            State.colorState
        )
    )


def page2() -> rx.Component:
    return rx.vstack(
        rx.button(
            "Click 2",
            on_click=State.ChangePage2Color,
        ),
        rx.text(
            State.colorState
        ),
    )


app = rx.App()
app.add_page(page1, on_load=State.set_client_token)
app.add_page(page2, on_load=State.set_client_token)
app.compile()


# Handle websocket disconnect events to avoid memory leaks when sessions are closed
orig_disconnect = app.event_namespace.on_disconnect

def disconnect_handler(sid):
    orig_disconnect(sid)

    clientToken, token = tokens_by_sid.get(sid, (None, None))
    print(f"Disconnect event received for {sid}. Removing {token} from shared {clientToken}")

    shared_sessions_by_token.get(clientToken, set()).discard(token)
    tokens_by_sid.pop(sid, None)

app.event_namespace.on_disconnect = disconnect_handler
Screen.Recording.2023-09-23.at.11.19.00.PM.mov

@picklelo picklelo closed this as completed Apr 3, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants