reflex icon indicating copy to clipboard operation
reflex copied to clipboard

Implement `throttle` and `debounce` as event actions

Open masenf opened this issue 4 months ago β€’ 2 comments

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)

https://github.com/reflex-dev/reflex/assets/1524005/5058b6bb-370d-4ef3-b752-cfea7cf22cd5

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.

masenf avatar Apr 15 '24 23:04 masenf

closes #1482 #2872

Lendemor avatar Apr 16 '24 17:04 Lendemor

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

picklelo avatar Apr 16 '24 17:04 picklelo