reflex/docs/events/background_events.md
2024-02-26 17:18:28 +01:00

176 lines
5.9 KiB
Markdown

```python exec
import reflex as rx
from pcweb import constants, styles
```
# Background Tasks
A background task is a special type of `EventHandler` that may run
concurrently with other `EventHandler` functions. This enables long-running
tasks to execute without blocking UI interactivity.
A background task is defined by decorating an async `State` method with
`@rx.background`.
Whenever a background task needs to interact with the state, **it must enter an
`async with self` context block** which refreshes the state and takes an
exclusive lock to prevent other tasks or event handlers from modifying it
concurrently. Because other `EventHandler` functions may modify state while the
task is running, **outside of the context block, Vars accessed by the background
task may be _stale_**. Attempting to modify the state from a background task
outside of the context block will raise an `ImmutableStateError` exception.
In the following example, the `my_task` event handler is decorated with
`@rx.background` and increments the `counter` variable every half second, as
long as certain conditions are met. While it is running, the UI remains
interactive and continues to process events normally.
```python demo exec
import asyncio
import reflex as rx
class MyTaskState(rx.State):
counter: int = 0
max_counter: int = 10
running: bool = False
_n_tasks: int = 0
@rx.background
async def my_task(self):
async with self:
# The latest state values are always available inside the context
if self._n_tasks > 0:
# only allow 1 concurrent task
return
# State mutation is only allowed inside context block
self._n_tasks += 1
while True:
async with self:
# Check for stopping conditions inside context
if self.counter >= self.max_counter:
self.running = False
if not self.running:
self._n_tasks -= 1
return
self.counter += 1
# Await long operations outside the context to avoid blocking UI
await asyncio.sleep(0.5)
def toggle_running(self):
self.running = not self.running
if self.running:
return MyTaskState.my_task
def clear_counter(self):
self.counter = 0
def background_task_example():
return rx.hstack(
rx.heading(MyTaskState.counter, " /"),
rx.chakra.number_input(
value=MyTaskState.max_counter,
on_change=MyTaskState.set_max_counter,
width="8em",
),
rx.button(
rx.cond(~MyTaskState.running, "Start", "Stop"),
on_click=MyTaskState.toggle_running,
),
rx.button(
"Reset",
on_click=MyTaskState.clear_counter,
),
)
```
## Task Lifecycle
When a background task is triggered, it starts immediately, saving a reference to
the task in `app.background_tasks`. When the task completes, it is removed from
the set.
Multiple instances of the same background task may run concurrently, and the
framework makes no attempt to avoid duplicate tasks from starting.
It is up to the developer to ensure that duplicate tasks are not created under
the circumstances that are undesirable. In the example above, the `_n_tasks`
backend var is used to control whether `my_task` will enter the increment loop,
or exit early.
## Background Task Limitations
Background tasks mostly work like normal `EventHandler` methods, with certain exceptions:
* Background tasks must be `async` functions.
* 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.
* Background tasks may not be directly called from other event handlers or background tasks. Instead use `yield` or `return` to trigger the background task.
## Low-level API
The `@rx.background` decorator is a convenience wrapper around the lower-level
`App.modify_state` async contextmanager. If more control over task lifecycle is
needed, arbitrary async tasks may safely manipulate the state using an
`async with app.modify_state(token) as state` context block. In this case the
`token` for a state is retrieved from `state.get_token()` and identifies a
single instance of the state (i.e. the state for an individual browser tab).
Care must be taken to **never directly modify the state outside of the
`modify_state` contextmanager**. If the code that creates the task passes a
direct reference to the state instance, this can introduce subtle bugs or not
work at all (if redis is used for state storage).
The following example creates an arbitrary `asyncio.Task` to fetch data and then
uses the low-level API to safely update the state and send the changes to the
frontend.
```python demo exec
import asyncio
import httpx
import reflex as rx
my_tasks = set()
async def _fetch_data(app, token):
async with httpx.AsyncClient() as client:
response = await client.get("https://api.github.com/zen")
async with app.modify_state(token) as state:
substate = state.get_substate(
LowLevelState.get_full_name().split("."),
)
substate.result = response.text
class LowLevelState(rx.State):
result: str = ""
def fetch_data(self):
task = asyncio.create_task(
_fetch_data(
app=rx.utils.prerequisites.get_app().app,
token=self.get_token(),
),
)
# Always save a reference to your tasks until they are done
my_tasks.add(task)
task.add_done_callback(my_tasks.discard)
def low_level_example():
return rx.vstack(
rx.text(LowLevelState.result),
rx.button(
"Fetch Data",
on_click=LowLevelState.fetch_data,
),
)
```