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

Client-side storage / State integration #1629

Merged
merged 10 commits into from
Aug 30, 2023
Merged

Conversation

masenf
Copy link
Collaborator

@masenf masenf commented Aug 20, 2023

Extend State to allow the storage of select Vars in the user's browser.

At compile time, extract all rx.Cookie and rx.LocalStorage typed state Vars
and save them in /utils/context.js as clientStorage for access on the
frontend.

When rx.Cookie or rx.LocalStorage values are set on the backend, the
frontend will also set them in the appropriate browser store based on the delta
received over the websocket. The clientStorage object provides options and
other information about how to persist the vars.

During initial hydrate event, values in the browser store will be read on the
frontend and sent to the backend as part of the event payload to be applied to
the state with HydrateMiddleware.

REF-172

Sample code

import reflex as rx


class State(rx.State):
    foo: str = rx.Cookie(max_age=60)
    bar: rx.Cookie = ""
    normal: str = "normal"

    def on_load(self):
        print(self.get_cookies())

    def set_foo_from_input(self, value: str):
        if value:
            self.foo = value
        return rx.set_value("set_foo_input", "")

    def set_bar_from_input(self, value: str):
        if value:
            self.bar = value
        return rx.set_value("set_bar_input", "")


class SubState(State):
    baz: str = rx.Cookie(same_site="strict")

    def set_baz_from_input(self, value: str):
        if value:
            self.baz = value
        return rx.set_value("set_baz_input", "")


class SubSubState(SubState):
    qux: str = rx.Cookie(name="qux", same_site="strict")
    quuc: str = rx.LocalStorage(name="this is quuc")

    def set_qux_from_input(self, value: str):
        if value:
            self.qux = value
        return rx.set_value("set_qux_input", "")

    def set_quuc_from_input(self, value: str):
        if value:
            self.quuc = value
        return rx.set_value("set_quuc_input", "")


def index() -> rx.Component:
    return rx.fragment(
        rx.color_mode_button(rx.color_mode_icon(), float="right"),
        rx.vstack(
            rx.heading("Welcome to Reflex!", font_size="2em"),
            rx.box("Cookie foo: ", rx.code(State.foo)),
            rx.box("Cookie bar: ", rx.code(State.bar)),
            rx.box("Cookie baz: ", rx.code(SubState.baz)),
            rx.box("Cookie qux: ", rx.code(SubSubState.qux)),
            rx.box("Local storage quuc: ", rx.code(SubSubState.quuc)),
            rx.button("Set Qux", on_click=rx.set_cookie("qux", "set outside of state")),
            rx.input(
                placeholder="Set foo",
                on_blur=State.set_foo_from_input,
                id="set_foo_input",
            ),
            rx.input(
                placeholder="Set bar",
                on_blur=State.set_bar_from_input,
                id="set_bar_input",
            ),
            rx.input(
                placeholder="Set sub_state.baz",
                on_blur=SubState.set_baz_from_input,
                id="set_baz_input",
            ),
            rx.input(
                placeholder="Set sub_sub_state.qux",
                on_blur=SubSubState.set_qux_from_input,
                id="set_qux_input",
            ),
            rx.input(
                placeholder="Set sub_sub_state.quuc",
                on_blur=SubSubState.set_quuc_from_input,
                id="set_quuc_input",
            ),
            padding_top="10%",
        ),
    )


app = rx.App()
app.add_page(index, on_load=State.on_load)
app.compile()

(see also integration/test_client_side.py)

TODO

  • fix up docstrings
  • fix up pyright issues

masenf added 4 commits August 20, 2023 14:30
Extend State to allow the storage of select Vars in the user's browser.

At compile time, extract all `rx.Cookie` and `rx.LocalStorage` typed state Vars
and save them in `/utils/context.js` as `clientStorage` for access on the
frontend.

When `rx.Cookie` or `rx.LocalStorage` values are set on the backend, the
frontend will also set them in the appropriate browser store based on the delta
received over the websocket. The clientStorage object provides options and
other information about how to persist the vars.

During initial hydrate event, values in the browser store will be read on the
frontend and sent to the backend as part of the event payload to be applied to
the state with HydrateMiddleware.

REF-172
Clean up and use a LocalStorage access helper found on stack overflow for use
in integration tests.

Move the `poll_for_navigation` contextmanager to new `utils` module so that it
can be more easily shared among integration tests.
Test rx.Cookie and rx.LocalStorage APIs with selenium
@masenf masenf force-pushed the masenf/client-side-storage branch from 3d3ffc2 to 016be96 Compare August 20, 2023 21:41
CI tests take longer, so get more of a buffer to avoid flaky test
@masenf masenf marked this pull request as ready for review August 20, 2023 21:51
@masenf masenf added this to the v0.2.6 milestone Aug 21, 2023
masenf added 2 commits August 21, 2023 12:18
Ensure that cookies do not persist on the backend after the user clears their
browser storage. Only values sent from the browser to the backend during
hydrate will be available in the state.
ensure that cookies are set with root path to avoid access issues.

fix #1628
Lendemor
Lendemor previously approved these changes Aug 21, 2023
Copy link
Collaborator

@Lendemor Lendemor left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks nice, good job!

@masenf
Copy link
Collaborator Author

masenf commented Aug 29, 2023

@picklelo ready for review when you get a chance.

@@ -1110,3 +1127,104 @@ def _convert_mutable_datatypes(
)

return field_value


class ClientStorageBase:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should these inherit from rx.Base to get Pydantic support? Most of our other classes inherit from there

secure: bool | None
same_site: str

def __new__(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we use __new__ instead of __init__ ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these two classes extend str, which is immutable, and thus cannot override __init__ to set values.

inst = super().__new__(cls, object, encoding or "utf-8", errors or "strict")
else:
inst = super().__new__(cls, object)
inst.name = name
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we use the rx.Base we shouldn't need to explicitly set these as it will handle it automatically

@picklelo picklelo merged commit 9fbc75d into main Aug 30, 2023
34 checks passed
@masenf masenf deleted the masenf/client-side-storage branch August 30, 2023 19:53
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

Successfully merging this pull request may close these issues.

3 participants