Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/events/background_events.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,4 +196,5 @@ Background tasks mostly work like normal `EventHandler` methods, with certain ex
- Background tasks cannot modify the state outside of an `async with self` context block.
- Background tasks may read the state outside of an `async with self` context block, but the value may be stale.
- Calling another event handler as a method of `self`, including one inherited from a parent state, runs it through the same proxy, so do it inside `async with self`. In the called handler `type(self)` is `StateProxy`; use `self.__class__` for the state class. See [Upgrading to Reflex 0.10](/docs/changelog/upgrading/upgrading-to-0-10/#calling-inherited-handlers-from-background-tasks).
- Awaiting an event handler through its state class, like `await State.increment()`, works in a background task. Outside `async with self` the event is queued and awaited, so it takes the lock on its own. Inside `async with self` a regular handler runs right away on the locked state, but a background handler raises `EventHandlerTypeError`; `yield` it instead. See [Awaiting Event Handlers](/docs/events/chaining-events/#awaiting-event-handlers).
- Background tasks may not be directly called from other event handlers or background tasks. Instead use `yield` or `return` to trigger the background task.
54 changes: 53 additions & 1 deletion docs/events/chaining_events.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,63 @@ def call_handler_example():
)
```

## Awaiting Event Handlers

An event handler accessed through its state class is awaitable. `await` runs it
right away, inside the current event, and gives back its return value. The
awaited handler runs on the same locked state as the caller, its `yield`s send
updates to the frontend as usual, and any backend events it yields or returns
are queued after the current event, as if the caller had yielded them;
frontend-only events are sent to the frontend immediately.

```python demo exec id=await-handler
class AwaitHandlerState(rx.State):
total: int = 0
last_added: int = 0

@rx.event
def add(self, amount: int) -> int:
"""Add to the total and return the new total."""
self.total += amount
self.last_added = amount
return self.total

@rx.event
async def add_twice(self, amount: int):
first = await type(self).add(amount)
second = await type(self).add(amount)
yield rx.toast(f"{first} then {second}")


def await_handler_example():
return rx.vstack(
rx.text(f"Total: {AwaitHandlerState.total}"),
rx.button("Add 5 twice", on_click=AwaitHandlerState.add_twice(5)),
)
```

Compared to calling the handler as a method of `self`, awaiting it converts
the arguments like an event from the frontend, sends an update when it yields,
and works for a handler of any state in the app, not only the caller's own
state.

```md alert info
# Rules for awaiting event handlers

- Only inside an event handler (or `app.modify_state`); awaiting a handler anywhere else raises an error.
- Calling the handler, like `State.add(5)`, only binds its arguments. Nothing runs until it is awaited, so it can still be returned or yielded as before. Each `await` runs it again.
- A background task awaiting a handler outside `async with self` queues the event and waits for it to finish; inside `async with self` the handler runs right away on the locked state.
- A background handler cannot be awaited while the state lock is held, as it would wait for that lock. `yield` it instead.
- Frontend-only events like `rx.redirect` cannot be awaited. `yield` or return them.
- Awaiting several handlers at once with `asyncio.gather` works, but they run concurrently on the same state and interleave at every `await` inside them. Await them one at a time when order matters.
- A task started from a handler, like `asyncio.ensure_future(State.add(1))`, keeps the state lock held until it finishes when it starts before the handler returns. If the handler returns first, the task's handler is queued like a yielded event and runs under its own lock.
```

## Returning Events From Event Handlers

So far, we have only seen events that are triggered by components. However, an event handler can also return events.

In Reflex, event handlers run synchronously, so only one event handler can run at a time, and the events in the queue will be blocked until the current event handler finishes.The difference between returning an event and calling an event handler is that returning an event will send the event to the frontend and unblock the queue.
In Reflex, event handlers run synchronously, so only one event handler can run at a time, and the events in the queue will be blocked until the current event handler finishes. The difference between returning an event and calling an event handler is that a returned event is queued and runs after the current handler finishes.

```md alert info
# Be sure to use the class name `State` (or any substate) rather than `self` when returning events.
Expand Down
32 changes: 32 additions & 0 deletions docs/events/decentralized_event_handlers.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,3 +159,35 @@ def process_form(state: FormState, data: dict):
# Process form data
return validate_data # Chain to another event
```

## Stateless Event Handlers

A function decorated with `@rx.event` whose first parameter is not a state is a
stateless event handler. It is registered like any other handler, can be used
as an event trigger, and can be awaited from other handlers, but it runs
without a state and without taking the state lock, so it never waits behind
the other events of the same client.

```python box
@rx.event
async def fetch_quote(symbol: str) -> float:
return await price_service.quote(symbol)


class PortfolioState(rx.State):
price: float = 0.0

@rx.event(background=True)
async def refresh(self, symbol: str):
price = await fetch_quote(symbol)
async with self:
self.price = price
```

Call external services from a [background task](/docs/events/background-events/)
so the client's state lock is not held while waiting: a stateless handler runs
inline wherever it is awaited, so the quote above is fetched outside
`async with self` and the lock is taken only to store it.

A stateless handler can still yield or return events, which run against the
state of the client that triggered it.
1 change: 1 addition & 0 deletions news/+awaitable-event-handlers.breaking.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
`@rx.event` now returns an `EventHandler` instead of the decorated function. A decorator stacked above `@rx.event` raises `TypeError` when the state class is created: apply other decorators below `@rx.event`. A module-level function decorated with `@rx.event` whose first parameter is not a state is now a stateless event handler, so calling it builds an event instead of running it; drop the decorator from plain helper functions.
14 changes: 14 additions & 0 deletions news/+awaitable-event-handlers.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
Event handlers are awaitable: `await State.handler(args)` inside another event handler runs it right away on the same locked state and returns its result, so a handler may return a value to the backend code awaiting it (returned events still chain, a returned coroutine still raises, and return values are not sent to the frontend yet). `@rx.event` returns the `EventHandler` itself, a function without a state parameter registers as a stateless handler that runs without the state lock, and `self.handler()` keeps calling the plain method as before.

```python
@rx.event
def add(self, amount: int) -> int:
self.total += amount
return self.total


@rx.event
async def add_twice(self, amount: int):
first = await type(self).add(amount)
yield rx.toast(f"total is now {await type(self).add(amount)}")
```
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
`@rx.event` now returns an `EventHandler` instead of the decorated function. A decorator stacked above `@rx.event` raises `TypeError` when the state class is created: apply other decorators below `@rx.event`. A module-level function decorated with `@rx.event` whose first parameter is not a state is now a stateless event handler, so calling it builds an event instead of running it; drop the decorator from plain helper functions.
14 changes: 14 additions & 0 deletions packages/reflex-base/news/+awaitable-event-handlers.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
Event handlers are awaitable: `await State.handler(args)` inside another event handler runs it right away on the same locked state and returns its result, so a handler may return a value to the backend code awaiting it (returned events still chain, a returned coroutine still raises, and return values are not sent to the frontend yet). `@rx.event` returns the `EventHandler` itself, a function without a state parameter registers as a stateless handler that runs without the state lock, and `self.handler()` keeps calling the plain method as before.

```python
@rx.event
def add(self, amount: int) -> int:
self.total += amount
return self.total


@rx.event
async def add_twice(self, amount: int):
first = await type(self).add(amount)
yield rx.toast(f"total is now {await type(self).add(amount)}")
```
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix a `TypeError` when an event handler is called with a proxied state value (such as `self.other(self.items)`) after one was called with a proxied value of a different kind, such as a dict.
Loading
Loading