[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
@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
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(...)