reflex-dev / reflex

🕸️ Web apps in pure Python 🐍

Home Page:https://reflex.dev

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

[REF-2578] Allow sending updates to frontend that aren't tracked as state vars

TimChild opened this issue · comments

Feature request

It would be great to have some way to directly update values in the frontend from the backend without having to also set a state var and specify the on_change event. This applies to several components:

  • Text based like input, textarea
  • Others like slider, select, switch etc.

I.e. Some way to update the components props without explicitly linking to state var. For example, with a textarea, I generally only care about getting the value from it on form submission (so I don't need to keep track of the value on every character change), but I would also like that upon a button click, the textarea gets populated with some starting text for the user to modify.

At the moment, the way to do it seems to be to set the value of the textarea to a state var, and then also set the on_change to update the value to allow editing the textarea.

While this works in many cases, it feels a bit inefficient since the State that is related to this could have expensive computed vars (so even with debouncing it could still add a lot of unnecessary overhead). Also, the contents of the textarea might be very large compared to any other var in the state, and since I don't actually care about it, I don't like that it then has to get sent back and forth all the time.

Here is a contrived example that demonstrates the issue and includes what I think might be a nice feature:

import time

import reflex as rx


class SomeState(rx.State):
    submitted_text: str = ""

    # Ideally wouldn't need this
    current_value: str = ""

    # for demo only
    _valid_submission: bool = False

    @rx.var
    def submission_valid(self) -> bool:
        # This exact case might still be fixed by a cached_var, but I think there are cases where it wouldn't
        # Or there are just many other rx.vars for some reason
        if not self.submitted_text:
            return False

        # This is a placeholder for a backend check that could be expensive
        # e.g. `has_anyone_else_submitted_text(text) that has to check a database etc`
        time.sleep(1)
        self._valid_submission = not self._valid_submission
        return self._valid_submission

    def handle_form_submission(self, data: dict[str, str]):
        self.submitted_text = data["text"]

    def handle_update_text_area(self):
        # Ideally, something like this would be nice <<<<<<<<<<<<<<<<< This would be the nice feature to add 
        yield rx.text_area.send_update(
            id="textarea_id",
            value="Starting text that could be loaded from db etc."
                  "\n\n\n\n\n\nalso might span many lines",
        )

    def handle_update_text_area_set_value(self):
        # Ideally wouldn't need this
        self.current_value = "Starting text that could be loaded from db etc."


IDEAL = False
if IDEAL:
    """
    Ideally, something like this would work
    """
    @rx.page(route="/", title="Example")
    def index() -> rx.Component:
        return rx.container(
            rx.heading("Example"),
            rx.form(
                rx.text_area(
                    id="textarea_id",
                    name="text",
                ),
                rx.button("Submit"),
                on_submit=SomeState.handle_form_submission,
            ),
            rx.cond(
                SomeState.submission_valid,
                rx.box("Valid submission", background_color="green"),
                rx.box("Invalid submission", background_color='red'),
            ),
            rx.text(f'Submitted text: {SomeState.submitted_text}'),
            rx.heading("Computed value:"),
            rx.text(SomeState.submission_valid),
            # Ideally would be able to use something like this
            rx.button("Set starting text", on_click=SomeState.handle_update_text_area),
        )

else:
    """
    Instead, have to do this
    """
    @rx.page(route="/", title="Example")
    def index() -> rx.Component:
        return rx.container(
            rx.heading("Example"),
            rx.form(
                rx.text_area(
                    name="text",
                    value=SomeState.current_value,
                    on_change=SomeState.set_current_value,
                ),
                rx.button("Submit"),
                on_submit=SomeState.handle_form_submission,
            ),
            rx.cond(
                SomeState.submission_valid,
                rx.box("Valid submission", background_color="green"),
                rx.box("Invalid submission", background_color='red'),
            ),
            rx.text(f'Submitted text: {SomeState.submitted_text}'),
            rx.heading("Computed value:"),
            rx.text(SomeState.submission_valid),
            # Ideally would be able to use something like this
            rx.button(
                "Set starting text", on_click=SomeState.handle_update_text_area_set_value
            ),
        )

If you run that with IDEAL=False you get the current implementation. After the first submission, it's basically impossible to write in the text area because the submission_valid var has to keep re-calculating even though we don't care about any changes until the next form submission.

If you instead run with IDEAL=True, the text area works nicely before and after submission, and the submission_valid is only calculated when there are new submissions... However, the button to update the textarea text obviously does not work.

I feel like some javascript could be used to do what I want in the ideal case, something like yielding a call_script that sets the value in the frontend? But I'm not exactly sure how to do this. My suggestion is that there is a helper function attached to the rx.textarea (and other similar components). Something like, send_update, which forms this call_script to directly updates the values in the frontend (targetted by id). Thus avoiding having to use persisted vars in the state.

It's not a major inconvenience for inputs where the on_change isn't triggered very often like select and switch, but it would still be nice to have this ability since it separates out the vars that you are actually interested in tracking vs those than can just be sent as one off updates.

Or maybe there is a better solution that exists already that I am missing... I'd be interested to hear thoughts on this.

I think this is a related issue:
#281 -- Setting an input/textarea value in an on_blur event

REF-2578

@TimChild We have rx.set_value which should be able to handle this. Perhaps we need to make it more prominent in different use cases as it's a bit hidden.

@picklelo, Well that appears to be exactly what I was looking for/suggesting ... And I see that it is right there in the docs, in a page that I had already read through 🤦

I think at the time that I read through that, I didn't realise the significance of that so I didn't think to come back to it.

Maybe it would be worth including that in an example along with text_area or slider since those components can really benefit from it. Something similar to my example above maybe?

Anyway, thank you very much for pointing that out!

I think there might also be an issue with the rx.set_state when used for the rx.select component:

import random
import reflex as rx

from ..templates import template

choices = ['a', 'b', 'c', 'd', 'e', 'f', 'g']

class SelectValueState(rx.State):
    selected_value: str = "a"

    def set_random_value_by_state_var(self):
        self.selected_value = random.choice(choices)

    def set_values_by_rx_event(self):
        yield rx.set_value('select-id', random.choice(choices))
        yield rx.set_value('another-select-id', random.choice(choices))
        yield rx.set_value('input-id', random.choice(choices))


@template(route="/select_with_id", title="Select with ID testing")
def index() -> rx.Component:
    return rx.container(
        rx.vstack(
            rx.hstack(
                rx.select(choices, value=SelectValueState.selected_value, on_change=SelectValueState.set_selected_value, id='select-id'),
                rx.text(f'Selected value is: {SelectValueState.selected_value}'),
            ),
            rx.hstack(
                rx.select(choices, id='another-select-id'),
                rx.text("Only expecting to see the value change in the select itself")
            ),
            rx.hstack(
                rx.button('Set random value by state var', on_click=SelectValueState.set_random_value_by_state_var),
                rx.button("Set random value with rx.set_value",
                          on_click=[
                              SelectValueState.set_values_by_rx_event,
                          ]),
            ),
            rx.hstack(
                rx.text("Showing that the rx.set_value works for an rx.input"),
                rx.input(id='input-id'),
            ),
        ),
    )

Renders something like this

image

I would expect that the event handler that yields the rx.set_value events would set a random value for both select components and the input compont, but it only works for the input component.
Setting a random value for the SelectValueState.selected_state does update the select value as expected.

(Maybe it is expected that the select which has a value=SelectValueState.selected_value is not updated by rx.set_value, but I'd definitely expect the other one to be updated)

@TimChild I was able to reproduce this, but i unfortunately don't see an easy fix with the radix select component.

If I rewrite it using the plain HTML select element, then it does actually work:

            rx.el.select(
                *[rx.el.option(choice) for choice in choices],
                placeholder="Select a value",
                id='another-select-id',
            ),

@masenf Thanks for looking into it.

I guess the workaround is just to use a state var to keep track of the value and handle on_change for that component then.

In case anyone else arrives here. I think this replicates similar behaviour to rx.set_value:

class SelectState(rx.State):
    selected: str
    
select = rx.select(['opt1', 'opt2'], value=SelectState.selected, on_change=SelectState.set_selected)

Then any other state could update the select via something like

class OtherState(rx.State):
    async def some_handler(self):
         return SelectState.set_selected('opt2')

Where you would otherwise return rx.set_value(...)