Skip to content

Use Async Effect

Overview

use_async_effect is a ReactPy hook that lets you run an async def effect, and keep it running across renders.

use_async_effect(effect)
use_async_effect(effect, dependencies=[value])

Reference

use_async_effect(effect, dependencies=..., shield=False)

Call use_async_effect at the top level of your component to run an asynchronous effect. ReactPy wraps the coroutine in an asyncio.Task, so it runs on the connection's event loop rather than blocking the render.

from reactpy import use_async_effect

Parameters

  • effect: An async def function taking no arguments. To run work in the background and stop it later, loop and handle cancellation — see running a long-lived effect.

  • dependencies (optional): Same four-way meaning as in use_effect, and the default is again not React's:

    Value Behaviour (verified)
    omitted (...) Inferred from the free variables of your closure.
    dependencies=[] Starts once, on mount.
    dependencies=[a, b] Restarts when the identity of a or b changes.
    dependencies=None Restarts after every render.
  • shield (optional): When True, an in-flight effect is not cancelled when the component unmounts or the effect is superseded. Default False.

Returns

None when given the effect, or a decorator when called without one.

Caveats

  • The effect must take zero arguments. ReactPy calls asyncio.create_task(func()) (core/hooks.py). The 1.x signature async def effect(stop): is therefore broken — and it fails silently. Verified: the component renders normally, the model carries no error, the browser shows nothing wrong; the only trace is Error in effect / TypeError: ... missing 1 required positional argument: 'stop' in the server log. If your async effect "does nothing", check the server log first.
  • There is no stop event to wait on. Cancellation is ordinary asyncio.CancelledError; a long-running effect should loop and break out of it.
  • Cancelling is also how clean-up happens: when dependencies change, the running task is cancelled before the new one starts.
  • The default dependency behaviour is inference, not "every render". See use_effect for the full table.
  • Effects run on the server. document and window do not exist.
  • An exception raised inside the coroutine is logged on the server only — no error reaches the model or the browser.
  • A new effect does not wait for the old one to finish unless you await cancellation, so two overlapping tasks can briefly run side by side. Use shield deliberately, not by default.

Usage

Importing use_async_effect

from reactpy import use_async_effect

Running a one-shot async effect

For a single await — a request, a slow lookup — write the coroutine straight:

@component
def stats(repo):
    data, set_data = use_state(None)

    async def effect():
        result = await fetch_stats(repo)
        set_data(result)

    use_async_effect(effect, dependencies=[repo])

    return html.p(data if data is not None else "loading...")

Setting state from the effect schedules the re-render; nothing else is needed.

Running a long-lived effect

To keep work running across renders, loop and honour cancellation. This is what ReactPy's version of React's setInterval example looks like:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import asyncio
from datetime import datetime, timezone

from reactpy import component, html, use_async_effect, use_state


@component
def clock():
    now, set_now = use_state(datetime.now(timezone.utc))

    async def effect():
        # Zero arguments. A long-running effect loops and honours cancellation.
        while True:
            set_now(datetime.now(timezone.utc))
            try:
                await asyncio.sleep(1)
            except asyncio.CancelledError:
                break

    use_async_effect(effect)

    return html.h1(now.strftime("%H:%M:%S"))
1
# TODO

The clock advances on its own — verified, one update per second. When dependencies change or the component unmounts, ReactPy cancels the task and the loop exits through CancelledError.

Do not wait for a stop event

ReactPy 1.x passed a stop event into async effects. That parameter no longer exists, and passing it is not reported anywhere the user can see. Write this:

async def effect():
    while True:
        do_work()
        try:
            await asyncio.sleep(1)
        except asyncio.CancelledError:
            break

Not this:

async def effect(stop):  # 🚩 silent failure
    while not stop.is_set():
        ...

Shielding an effect from cancellation

shield=True lets an in-flight effect run to completion even as the component unmounts — useful for finishing a request whose result must not be discarded:

use_async_effect(effect, dependencies=[], shield=True)

Verified: with shield=True a sleeping effect reached its completion line after the layout exited; with the default it was cancelled at that same moment. Leave this off unless you specifically want work to outlive the component, since it also means the effect keeps holding references and consuming the loop.

Cleanup

Async effects do not return a clean-up function; cancellation is the clean-up. Put teardown in the except asyncio.CancelledError branch, and re-raise so asyncio can finish cancelling:

async def effect():
    handle = open_connection()
    try:
        while True:
            await handle.poll()
    except asyncio.CancelledError:
        handle.close()
        raise

Re-raising matters: swallowing CancelledError leaves the task in a state asyncio did not expect.

Choosing between use_effect and use_async_effect

Need Use
A synchronous side effect use_effect
Anything await-based use_async_effect
A repeating timer, polling, a subscription stream use_async_effect with a loop
Work that must block the render neither — do it during render, or raise

Passing an async def to use_effect fails loudly, which is the one mismatch ReactPy does report: TypeError: use_effect does not support async functions. Use use_async_effect instead.

Authors: Archmonger