Skip to content

Use Effect

Overview

use_effect is a ReactPy hook that lets you synchronize a component with an external system.

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

Reference

use_effect(effect, dependencies=...)

Call use_effect at the top level of your component to run an effect after rendering.

from reactpy import use_effect

Parameters

  • effect: A function taking no arguments. ReactPy calls it after the component renders. It may return a clean-up function (also taking no arguments), which ReactPy runs before the next time this effect runs and again when the component unmounts.

    The effect must be pure, and it must not be declared async def — ReactPy raises for that; use use_async_effect instead.

  • dependencies (optional): Controls when the effect re-runs. Unlike React, this argument has four distinct settings, and the default is not React's:

    Value Behaviour (verified)
    omitted (...) Inferred. ReactPy reads the free variables of your closure (core/hooks.py, _try_to_infer_closure_values) and re-runs when any of them changes. A closure that reads nothing runs once.
    dependencies=[] Runs once, after mount.
    dependencies=[a, b] Runs when a or b changes.
    dependencies=None Runs after every render.

    "Changes" means strictly_equal (core/hooks.py), not is: the two values must share a type, two locally-defined functions or lambdas are compared by bytecode, and everything else falls through to == with is as the last resort. A rebuilt list or dict with equal contents does not re-run the effect. Use Memo documents the same function in full.

Returns

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

Caveats

  • The default is inference, not "every render". In React, omitting the dependency array means the effect runs after every render. In ReactPy it means "re-run when the variables my closure reads change". Code ported from React will therefore behave differently — pass dependencies=None if you really want every-render behaviour, though usually you want to be explicit instead.
  • Inference is structural, not declared, and it does not compare by id. A closure that happens to read an extra local variable gains an extra dependency, and the comparison is strictly_equal — type, then bytecode for local functions, then == — so a rebuilt-but-equal container counts as unchanged. If the effect runs more or less often than you expect, look at which names it actually reads, and then at how those values compare.
  • Effects run on the server, after rendering, not in the browser. There is no DOM to touch: document and window do not exist here. There is no document or window to reach for, and no ReactPy equivalent of attaching a listener to a node — see Responding to Events for the supported way to react to the browser.
  • Children's effects run before their parent's, and siblings run in reverse order. Verified output: ['child effect b', 'child effect a', 'parent effect']. Two effects in one component run in declaration order. Do not rely on React's parent-first or source-order guarantees.
  • Passing async def to use_effect fails loudly: TypeError: use_effect does not support async functions. Use use_async_effect instead. The error lands in the model's error field and the component stops rendering.
  • An exception raised inside an effect body is logged on the server only; the model carries no error and the UI looks unchanged. Check the server log.
  • Effects are not a substitute for computing values. Derive them during render or with use_memo.
  • There is no use_layout_effect, use_insertion_effect, or use_imperative_handle. Nothing runs synchronously during the render phase.

Usage

Importing use_effect

from reactpy import use_effect

Describing an effect

Pass the setup function directly:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
@component
def live_feed():
    latest, set_latest = use_state("waiting for a message...")

    def effect():
        # Runs once on mount; the returned function runs on unmount.
        unsubscribe = ticker.subscribe("news", set_latest)
        return unsubscribe

    use_effect(effect, dependencies=[])

    async def handle_click(event):
        ticker.publish("news", "Hello!")

    return html.div(
        html.h1(latest),
        html.button(
            {"onClick": handle_click},
            "Publish",
        ),
    )
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
"""A stand-in for any external system with subscribe/unsubscribe semantics."""


class Ticker:
    def __init__(self):
        self.topics = {}

    def subscribe(self, topic, callback):
        self.topics.setdefault(topic, []).append(callback)

        def unsubscribe():
            self.topics[topic].remove(callback)

        return unsubscribe

    def publish(self, topic, message):
        for callback in list(self.topics.get(topic, [])):
            callback(message)

    def count(self):
        return sum(len(v) for v in self.topics.values())


ticker = Ticker()
1
# TODO

Or use it as a decorator, which is equivalent:

@use_effect
def effect():
    print("mounted")

Telling ReactPy when to re-run the effect

Pass a dependency list to re-run only when something relevant changes:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@component
def live_feed():
    topic, set_topic = use_state("news")
    latest, set_latest = use_state("")

    def effect():
        # Re-runs whenever `topic` changes; the returned clean-up unsubscribes
        # the previous topic first, so you never keep two subscriptions.
        unsubscribe = ticker.subscribe(topic, set_latest)
        return unsubscribe

    use_effect(effect, dependencies=[topic])

    async def handle_topic_click(event):
        set_topic("sports" if topic == "news" else "news")

    async def handle_click(event):
        ticker.publish(topic, "ping")

    return html.div(
        html.p(f"Subscribed to: {topic}"),
        html.p(latest or "(nothing yet)"),
        html.button(
            {"onClick": handle_topic_click},
            "Switch topic",
        ),
        html.button({"onClick": handle_click}, "Publish"),
    )
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
"""A stand-in for any external system with subscribe/unsubscribe semantics."""


class Ticker:
    def __init__(self):
        self.topics = {}

    def subscribe(self, topic, callback):
        self.topics.setdefault(topic, []).append(callback)

        def unsubscribe():
            self.topics[topic].remove(callback)

        return unsubscribe

    def publish(self, topic, message):
        for callback in list(self.topics.get(topic, [])):
            callback(message)

    def count(self):
        return sum(len(v) for v in self.topics.values())


ticker = Ticker()
1
# TODO

Switching topic tears down the old subscription before creating the new one — verified, the subscriber count stays at 1 across switches and drops to 0 on unmount.

Omitting the dependencies argument

use_effect(effect)               # inferred from what the closure reads

The example below makes inference visible. The effect reads count but not label, so renaming re-renders the component without re-running the effect:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@component
def logger():
    count, set_count = use_state(0)
    label, set_label = use_state("counter")
    runs, set_runs = use_state([])

    def effect():
        # No `dependencies` argument, so ReactPy infers them from the names
        # this closure reads. Only `count` is a real dependency here -- the
        # setter is stable, and `label` is not read at all. So pressing
        # Rename re-renders the component but does NOT re-run this effect.
        set_runs(lambda previous: [*previous, f"effect ran with count={count}"])

    use_effect(effect)

    async def handle_count_click(event):
        set_count(count + 1)

    async def handle_label_click(event):
        set_label(label + "!")

    return html.div(
        html.h1(count),
        html.button({"onClick": handle_count_click}, "Add 1"),
        html.button({"onClick": handle_label_click}, "Rename"),
        html.p(f"Label: {label}"),
        html.ul([html.li({"key": i}, line) for i, line in enumerate(runs)]),
    )
1
# TODO

Pressing Rename twice adds no new log line — verified. Be aware this works because of which names the closure reads; it is implicit, so many teams prefer to spell the list out.

Passing an empty dependency array

use_effect(effect, dependencies=[])   # once, after mount

For mount-only work — subscribing, starting a timer, reading a config — say so explicitly.

Passing None

use_effect(effect, dependencies=None)  # after every render

This is the closest ReactPy gets to React's "no array" behaviour. It re-runs the effect after every render, verified. Reach for it rarely: it usually means the effect is doing work that belongs in render or in use_memo.

Adding clean-up

Return a function from the effect. ReactPy calls it before the next run of that effect and when the component unmounts:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@component
def app():
    connected, set_connected = use_state(False)

    async def handle_connected_click(event):
        set_connected(not connected)

    return html.div(
        html.button(
            {"onClick": handle_connected_click},
            "Connect" if not connected else "Disconnect",
        ),
        *([] if not connected else [connection()]),
    )
1
# TODO

Connecting and disconnecting runs the pair in order — verified: connected, disconnected, connected across connect / disconnect / reconnect.

Clean-up is the difference between a leak and a working app

Because ReactPy components live in long-lived server processes, a forgotten subscription is not a brief flash of stale data — it is a permanent leak that outlives every browser tab that opened it. Every subscribe, every timer, every held connection needs a clean-up function.

Effects and the request lifecycle

An effect runs in the connection's own task, after the render that scheduled it. It can call setters, and those schedule further renders. Two consequences:

  • An effect that awaits gives the event loop control. Long awaits belong in use_async_effect, which is built for it.
  • Setting state from an effect that depends on that same state loops forever. If an effect seems to spin, read what it sets and what it depends on.

Reading the latest state inside an effect

An effect closes over the render that created it. If it then runs for a long time — an interval callback, a subscription handler — it can act on values that are no longer current. Two ways to handle it:

  • Put the value in the dependency list, so a change tears the effect down and starts a new one with fresh values.
  • Keep the value in a use_ref box and read ref.current when you need it. The box persists across renders, so readers see the newest value — as long as the write happens somewhere legal. Do not assign ref.current from the component body: a render-phase write is exactly the impurity Use Ref warns about, and ReactPy has no Strict Mode to catch it. Assign it where the value is already produced — the event handler that changed it — or in its own effect, which runs after the render.

If an effect reads state it does not declare, that is usually a sign the effect should be re-created when that state changes — reach for the dependency list first.

Authors: Archmonger