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

Implement throttle and debounce as event actions #3091

Merged
merged 3 commits into from
Apr 27, 2024

Conversation

masenf
Copy link
Collaborator

@masenf masenf commented Apr 15, 2024

Generic client side throttling and debounce for events.

Used just like .stop_propagation and .prevent_default, so applicable to all event handler or event spec objects.

Due to how events are handled, there isn't currently a way to apply these to only a single event in the chain. If multiple event handlers are specified and any of them have debounce or throttle, then it will apply to all of the other events as well.

If the same function is throttled or debounced at the same limit/delay by multiple event triggers, these will all apply in the same domain. If different limit or delay is used, then they will be debounced or throttled separately.

Sample Code

import time


import reflex as rx


class State(rx.State):
    """The app state."""
    _t_events: int = 0
    _d_events: int = 0
    last_t_event: str
    last_d_event: str
    slider_value: list[int] = 30

    def handle_mouse_move_t(self):
        self._t_events += 1
        self.last_t_event = f"throttle moved: {self._t_events} {time.time()}"
        print(self.last_t_event)

    def handle_mouse_move_db(self):
        self._d_events += 1
        self.last_d_event = f"debounce moved: {self._d_events} {time.time()}"
        print(self.last_d_event)

    def handle_slider_change(self, value):
        self.slider_value = value[0]


def index() -> rx.Component:
    return rx.hstack(
        rx.box(
            rx.vstack(
                rx.heading("Throttle"),
                rx.box(height="50vh"),
                State.last_t_event,
                align="center",
                padding_top="10vh",
            ),
            on_mouse_move=lambda: State.handle_mouse_move_t.throttle(500),
            height="85vh",
            width="50vw",
            background="linear-gradient(#e66465, #9198e5)"
        ),
        rx.box(
            rx.vstack(
                rx.heading("Debounce"),
                rx.box(height="50vh"),
                State.last_d_event,
                align="center",
                padding_top="10vh",
            ),
            on_mouse_move=State.handle_mouse_move_db.debounce(500),
            height="85vh",
            width="50vw",
            background="linear-gradient(#9198e5, #e66465)"
        ),
    ), rx.cond(
        State.is_hydrated,
        rx.vstack(
            rx.heading(State.slider_value),
            rx.slider(
                default_value=[State.slider_value],
                min=0,
                max=100,
                on_change=State.handle_slider_change.debounce(50),
            ),
            align="center",
        )
    )


app = rx.App()
app.add_page(index)
Screen.Recording.2024-04-15.at.16.25.55.mov

Edit 2024-04-16: added a debounced slider example. Note it's important to wrap is the is_hydrated cond, otherwise the default_value after refreshing will be the initial value, not the last value.

Apply these settings similar to `stopPropagation` or `preventDefault` to
influence how the event chain is processed on the frontend.
Lendemor
Lendemor previously approved these changes Apr 15, 2024
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.

Nice, the UX will be much better for high fire-rate events !

Could we add a test when the event handler is a lambda? I expect the behaviour to be the same, but might as well be sure.

Fix integration test to allow one less throttled event than expected. Avoid
flaky CI due to race conditions in the timing of clicks vs throttle limit.
rx.button(
"Throttle",
id="btn-throttle",
on_click=lambda: EventActionState.on_click_throttle.throttle(
Copy link
Collaborator

Choose a reason for hiding this comment

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

I shoud have been more specific in the test I wanted, but since lambda are used to pass pre-set parameter, when we do, do we use

lambda value: EventActionState.on_change_throttle_with_args(value, "arg1").throttle(200).stop_propagation

or

(lambda value: EventActionState.on_change_throttle_with_args(value, "arg1")).throttle(200).stop_propagation

?
Though maybe it's more important to have this particular example in the docs than the tests.

@Lendemor
Copy link
Collaborator

closes #1482 #2872

@picklelo
Copy link
Contributor

This is a much needed feature, let me think a bit more on the final API but I like it

Copy link
Contributor

@picklelo picklelo left a comment

Choose a reason for hiding this comment

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

works great with my color picker

@picklelo picklelo merged commit 3564df7 into main Apr 27, 2024
46 checks passed
@masenf masenf deleted the masenf/generic-debounce-throttle branch June 25, 2024 02:21
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