Skip to content

Use Callback

Overview

use_callback is a ReactPy hook that caches a function, replacing it only when one of its listed dependencies changes.

cached_handler = use_callback(handler, dependencies=[dep_1, dep_2])

It is the function-shaped counterpart of use_memo: same rules, same dependency behaviour, but the thing you cache is a callable rather than a value.

You probably do not need it

In React, useCallback earns its place by keeping a child from re-rendering behind memo and by keeping effect dependencies from thrashing. Neither reason applies to ReactPy. There is no render bailout and no memo, and a function you define in the component body already counts as an unchanged dependency, because ReactPy compares local functions by their code rather than by identity. So the effect-churn problem useCallback exists to solve does not occur here.

What it does give you is a genuinely stable function object, which matters only when the function leaves ReactPy and something else keys on its identity — a subscription registry, an event bus, a cached table. If none of that describes your code, write a plain def and skip this page.


Reference

use_callback(function, dependencies)

Call use_callback at the top level of your component to cache an event handler or other callback.

from reactpy import use_callback

Parameters

  • function: The function you want to keep. It must be pure in the sense that matters here: same closure, same behaviour.
  • dependencies: The values the function reads. ReactPy replaces the cached function when any of them changes. Three shapes, identical to use_memo:

    You pass Meaning
    dependencies=[a, b] Replace the function when a or b changes.
    omitted (the default) Infer the dependencies from the function's closure.
    dependencies=None Return a new function on every render — no caching at all.

Returns

The cached function — either the one from a previous render, or, if a dependency changed, the one you just passed in.

Caveats

  • The function you pass is not discarded, it is stored. ReactPy does not call it; it keeps the object. That is why caching a function that closes over state can hand you an old closure — see A cached handler sees stale values.
  • dependencies=[] pins the function forever. The same object comes back on every render, including the one holding the values it closed over on the first render. This is the single most common use_callback bug, and ReactPy will not warn you.
  • Listing a dependency you read is not optional. Forgetting one produces a handler that quietly acts on outdated values. Nothing checks this for you.
  • Stable identity does not mean stable behaviour. A function can be the same object across renders and still be wrong, because identity was the point and the closure went stale. Cache identity when something external needs identity; do not use it as a performance gesture.
  • Comparison follows strictly_equal, not identity — see the equivalent caveat in use_memo: same type first, code comparison for local functions, == otherwise.
  • The decorated name is the function, not a wrapper. @use_callback(dependencies=[x]) receives your function and returns a function, so the name still calls fine — unlike use_memo, where the name becomes the value.
  • Like every hook, call it only at the top level — never in a loop, a conditional, or after an early return.

Usage

When a stable identity matters

Here is the case use_callback is actually for. The component subscribes to a bus that lives outside ReactPy. That bus stores handlers in a list and skips duplicates by identity — so a new function object on each render registers another handler, and every handler receives each message.

Press Re-render three times. handlers registered stays at 1, because the handler really is the same object each time. Press Publish and it is delivered once. Replace the use_callback line with a plain def and the same interaction registers four handlers.

 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
29
30
31
32
@component
def listener():
    renders, set_renders = use_state(0)
    heard, set_heard = use_state(0)

    # The registry lives outside ReactPy, so nothing deduplicates it for you.
    # A plain `def` here would be a brand new function object on every render,
    # and the registry would therefore collect another handler each time --
    # all of which receive every message. Pinning the identity keeps exactly
    # one handler, however often this component renders.
    handler = use_callback(
        lambda message: set_heard(lambda previous: previous + 1), dependencies=[]
    )

    def subscribe():
        bus.subscribe(handler)

    use_effect(subscribe, dependencies=None)

    async def handle_renders_click(event):
        set_renders(renders + 1)

    async def handle_click(event):
        bus.publish("ping")

    return html.div(
        html.p(f"renders: {renders}"),
        html.p(f"handlers registered: {len(bus.handlers)}"),
        html.p(f"this component heard: {heard}"),
        html.button({"onClick": handle_renders_click}, "Re-render"),
        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
25
26
"""A stand-in for any registry that lives outside ReactPy.

This is the shape of code where a callback's identity actually matters: the
registry is ordinary Python, so nothing deduplicates it for you -- it stores
handlers by identity, and a *new* function object on each render means a new
entry each render.
"""


class Bus:
    def __init__(self):
        self.handlers = []
        self.handled = []

    def subscribe(self, handler):
        # `in` on a list of plain functions compares by identity, which is
        # exactly what a new `def` per render changes.
        if handler not in self.handlers:
            self.handlers.append(handler)

    def publish(self, message):
        for handler in list(self.handlers):
            handler(message)


bus = Bus()
1
# TODO

Note the heard updater: set_heard(lambda previous: previous + 1). It reads no state, which is what lets this handler keep an empty dependency list honestly — an updater function gets the current value at the moment the setter runs, so the handler does not need to close over anything.

A cached handler sees stale values

The notes app below caches a handler with dependencies=[] while the handler reads draft. ReactPy keeps the first function, which closed over an empty draft, so every click adds a blank note no matter what you type.

 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
29
30
31
32
33
@component
def notes():
    notes_list, set_notes_list = use_state([])
    draft, set_draft = use_state("")

    # `draft` is read here, but `dependencies=[]` promises the function never
    # changes -- so ReactPy keeps handing back the *first* function, which
    # closed over an empty `draft`. Each click adds a blank note no matter what
    # you typed. Listing what the handler reads is what fixes it.
    async def add_note_handler(event):
        set_notes_list([*notes_list, draft])

    add_note = use_callback(add_note_handler, dependencies=[])

    async def handle_draft_change(event):
        set_draft(await event.target.value)

    return html.div(
        html.h1("Notes"),
        html.ul(
            [
                html.li({"key": str(i)}, note or "(blank)")
                for i, note in enumerate(notes_list)
            ]
        ),
        html.input(
            {
                "value": draft,
                "onChange": handle_draft_change,
            }
        ),
        html.button({"onClick": add_note}, "Add note"),
    )
1
# TODO

Type something and press Add note: the list gains (blank). The handler is working perfectly — on the render it was created on.

Listing what the handler reads

The fix is to name the values the function reads. Now the cached function is replaced whenever notes_list or draft changes, so a click always sees what is currently in the box — while the identity still stays put between 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
29
30
31
32
@component
def notes():
    notes_list, set_notes_list = use_state([])
    draft, set_draft = use_state("")

    # List what the handler reads and the cached function is replaced exactly
    # when those values change: the click always sees the current draft, while
    # the identity still stays put between changes.
    async def add_note_handler(event):
        set_notes_list([*notes_list, draft])

    add_note = use_callback(add_note_handler, dependencies=[notes_list, draft])

    async def handle_draft_change(event):
        set_draft(await event.target.value)

    return html.div(
        html.h1("Notes"),
        html.ul(
            [
                html.li({"key": str(i)}, note or "(blank)")
                for i, note in enumerate(notes_list)
            ]
        ),
        html.input(
            {
                "value": draft,
                "onChange": handle_draft_change,
            }
        ),
        html.button({"onClick": add_note}, "Add note"),
    )
1
# TODO

Type something and press Add note: the text lands. These two files differ by one line; the behaviour difference is the whole point.

Omitting the dependency list

Leave out dependencies and ReactPy infers them from the function's closure, exactly as it does for use_memo and use_effect. For a handler that reads component state this is usually what you want, and it removes the "forgot a dependency" class of bug. Omitting is not the same as dependencies=[] — the inferred list is built from what the function actually reads, which is rarely empty.


Troubleshooting

My handler acts on old values

You cached it with an empty or incomplete dependency list. The function is the same object, and so is everything it closed over. Either list the values it reads, or stop the value from being read at all by using an updater — set_items(lambda previous: [*previous, new]) instead of set_items([*items, new]).

My handler never fires

Check the attribute name. {"on_click": handler} renders without complaint, lands in eventHandlers, and is never called — the client registers a DOM listener for the key with on stripped, so the underscore spelling subscribes to an event named _click that no browser ever fires. The key must be the DOM event name in camelCase ("onClick"), and the value must be your function, not a call to it. A wrong name is invisible: the only difference is that nothing happens.

I still get a new function every render

  • You passed dependencies=None, which means exactly that.
  • A dependency is rebuilt each render and does not compare equal — a plain class instance without __eq__, for instance.
  • The function is defined outside the component and you expect it to be cached; use_callback caches what you hand it, per component instance.

Do I need this to stop a child re-rendering?

No — that mechanism does not exist in ReactPy. See the note at the top of the page: children re-render whenever their parent does, regardless of prop or handler identity.

Authors: Archmonger