reflex
reflex copied to clipboard
Call event handlers from other event handlers
Currently the only way to call another event handler is to return it and create a chain. However this leads to poor code reusability in many cases.
Example in #596
class State(pc.State):
count1: int = 0
count2: int = 1
sum_count: int = 0
def increment_count1(self):
time.sleep(1)
self.count1 += 1
def add_counts(self):
self.sum_count = self.count1 + self.count2
def increment_add(self):
self.increment_count1()
self.add_counts()
Currently the given increment_add function won't work in Pynecone. We also can't use an event chain like on_click=[State.increment_count1, State.add_counts] because event two relies on event one and it executes asynchronously. This only leaves us with one option
This should be very possible to implement.
@picklelo any ideas on where to get started on this?
@lucashofer So currently when you do something like self.event_handler
within the code, it returns an EventHandler
object rather than the function itself. To call the function, you need to do self.event_handler.fn(self)
.
I made a little app below to demonstrate:
"""Welcome to Pynecone! This file create a counter app."""
import pynecone as pc
import random
class State(pc.State):
"""The app state."""
count = 0
def print_state(self):
"""Print the state."""
print(self.count)
def increment(self):
"""Increment the count."""
self.print_state.fn(self)
self.count += 1
def decrement(self):
"""Decrement the count."""
self.print_state.fn(self)
self.count -= 1
def random(self):
"""Randomize the count."""
self.print_state.fn(self)
self.count = random.randint(0, 100)
def index():
"""The main view."""
return pc.center(
pc.vstack(
pc.heading(State.count),
pc.hstack(
pc.button("Decrement", on_click=State.decrement, color_scheme="red"),
pc.button(
"Randomize",
on_click=State.random,
background_image="linear-gradient(90deg, rgba(255,0,0,1) 0%, rgba(0,176,34,1) 100%)",
color="white",
),
pc.button("Increment", on_click=State.increment, color_scheme="green"),
),
padding="1em",
bg="#ededed",
border_radius="1em",
box_shadow="lg",
),
padding_y="5em",
font_size="2em",
text_align="center",
)
# Add state and page to the app.
app = pc.App(state=State)
app.add_page(index, title="Counter")
app.compile()
What do you think's the best way do handle this? It is technically possible already - so we can either document it or somehow add some magic to clean up the API.
The only thing is - we still need to support returning event handlers to support chaining, etc. So I don't think we can easily just convert them all to the underlying functions.
Sorry for the late reply on this—currently finishing up my diss.
I think some magic should be added in the API. It took me quite a bit of searching through the code to realize state functions are converted to EventHandlers behind the scenes.
So to state the problem overall, just to make sure my understanding is correct:
- From a pure Python perspective, class functions should be callable by other class functions.
- Behind the scenes, State functions need to processed as event handlers when called either from a pc component or as part of an even chain.
To address 1) I think the state functions should be left alone. This will allow class functions to call one another directly as would be assumed.
Then behind the scenes we can handle turning those state functions into Event Handlers. Specifically, within the state the current __init_subclass__
events = {
name: fn
for name, fn in cls.__dict__.items()
if not name.startswith("_") and isinstance(fn, Callable)
}
for name, fn in events.items():
event_handler = EventHandler(fn=fn)
setattr(cls, name, event_handler)
could have the final line changed to
cls.event_handlers[name] = event_handler
However, we still want to keep the API the same which means whenever an event is referenced we need the form on_click=State.event
. Since in the structure I propose above, the State.event function is just a function, we need to use __qualname__
to get the name and state from which we can extract the correct event handler from the state's event handlers dict. This could be done in the state.process
function and similar code already exists in middleware.HydrateMiddleware
in conjunction with utils.format_event_handler.
Anyway, this is a rough idea and could use refinement, but I think long term will be more intuitive for users by abstracting away the machinery behind event handlers.