Skip to content

Use Ref

Overview

use_ref is a ReactPy hook that gives you a mutable box which survives re-renders. Writing to it never causes a render.

value_box = use_ref(initial_value)

A ref is for values your component uses but never renders. It is a plain Python object holding one field, so reading and writing it is just attribute access — no setter, no scheduling, no re-render.

There is no DOM ref in ReactPy

React's useRef is often used to grab a DOM node — inputRef.current.focus(), videoRef.current.play(), myDiv.current.scrollIntoView(). That use does not exist in ReactPy. Your components run on the server, where there is no DOM, and ReactPy has no mechanism for handing a browser node to Python. There is no ref_current attribute, no callback ref, nothing to attach a node to. Anything you write as a "ref_..." attribute is an unknown attribute the client silently ignores.

Express browser behavior through state and ordinary attributes instead: {"autoFocus": True}, {"value": text}, {"disabled": not ready}. If you need genuinely imperative browser code, that belongs in a client-side app, where your Python runs in the browser.


Reference

use_ref(initial_value)

Call use_ref at the top level of your component to declare a value box.

from reactpy import use_ref

Parameters

  • initial_value: The value the box starts with. It can be any Python value, including a mutable one.

Returns

A reactpy.utils.Ref object — the same object on every render of that component. It has two members:

  • current: The value. Read and write it directly: value_box.current = new_value. Reading it does not schedule a render.
  • set_current(new_value): Assigns current and returns the value it replaced. Because it is a call rather than an assignment, it fits where a statement cannot go — inside a lambda, for example.

Caveats

  • initial_value is evaluated on every render. use_ref(build_something_expensive()) calls build_something_expensive() each time the component renders, then throws all but the first result away. Verified: four renders, four calls. To build something once, start empty and fill it in:

    index = use_ref(None)
    if index.current is None:
        index.current = build_something_expensive()
    
  • Writing to a ref never triggers a render. That is the point, and also the trap: if the value is something the screen shows, it will not update. See Ref versus state.

  • Mutating a ref does not invalidate anything. Dependency comparison treats the box as unchanged no matter what you write into it, because it compares equal to itself. Listing a ref in a dependencies list is therefore useless — the effect or memo will never re-run because of it. Verified: an effect with dependencies=[box] ran once while its box.current was written to three times.
  • A ref is unhashable. hash(use_ref(0)) raises TypeError, so it cannot be a dict key or a set member. Its == compares contents: two separate Ref(0) boxes are equal.
  • Render value_box.current, not the box. html.p(my_ref) does not raise; it renders the repr, so the reader sees Ref(0) on the page. Passing a ref as an attribute value fails at serialization instead.
  • Like every hook, call it only at the top level — never in a loop, a conditional, or after an early return. ReactPy does not check: shuffling which hooks run between renders yields a scrambled component and a cryptic IndexError, not a helpful message.
  • ReactPy has no Strict Mode, so nothing runs your component twice to catch impure code. A ref written to during rendering is exactly as unsafe here as in React, and nothing will warn you. See Keeping Components Pure.

Usage

Ref versus state

The question is not "which is cheaper" but who needs to know. If changing the value must update the screen, it is state. If only the code needs it, it is a ref.

The two counters below start together and then drift apart, because only one of them causes a render. The ref counter's new value sits in the box unseen until something else renders the component.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
@component
def counter():
    # Two buttons, one counter each. Only the state one causes a render, so the
    # numbers only come back into sync on a render triggered by something else.
    state_count, set_state_count = use_state(0)
    ref_count = use_ref(0)

    def bump_state(event=None):
        set_state_count(state_count + 1)

    def bump_ref(event=None):
        ref_count.current += 1  # writes in place: no render is scheduled

    return html.div(
        html.p(f"state: {state_count}"),
        html.p(f"ref: {ref_count.current}"),
        html.button({"onClick": bump_state}, "Increment state"),
        html.button({"onClick": bump_ref}, "Increment ref"),
    )
1
# TODO

The usual split:

You need... Use
A value the UI renders use_state
A value only handlers or effects read use_ref
A value that drives the UI and is written by a long-lived loop Both: state for the UI, ref for the loop
Something built once, then read-only use_ref, with the guard
A mutable value the UI renders use_state — replace the object instead of mutating it; see Updating Objects in State

Remembering a value built once

Because the box is the same object across renders, it is the natural place for something expensive to construct and cheap to read: a parsed file, an index, a client object. Start it empty so the expensive call is not repeated on every render.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
def build_word_index():
    """Stand-in for something slow: reading a file, opening a connection, etc."""
    return {word: len(word) for word in "react py is a python ui library".split()}


@component
def word_counter():
    # Built once, on the first render, and then reused for the life of the
    # component -- no `dependencies`, no invalidation, nothing to keep in sync.
    index: Ref[dict[str, int] | None] = use_ref(None)
    if index.current is None:
        index.current = build_word_index()

    return html.div(
        html.h1("Word lengths"),
        html.ul(
            [
                html.li(f"{word}: {length}")
                for word, length in sorted(index.current.items())
            ]
        ),
    )
1
# TODO

If the value should be rebuilt when some input changes, a ref is the wrong tool — that is what a dependencies list is for. See use_memo.

Sharing a value with a long-lived effect

An use_async_effect loop keeps running across renders, so it closes over the state values from the render that started it. A ref sidesteps this: it is the same object every render, so the loop reads whatever the handlers wrote after it started.

The stopwatch below keeps the start timestamp in a ref. The ticking effect reads it and the buttons write it, none of which needs a render of its own. The value the screen shows, elapsed, is state.

 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
34
35
36
37
38
39
40
41
42
43
44
@component
def stopwatch():
    # What the UI renders lives in state. What only the handlers and the
    # ticking effect read -- and which must never trigger a render of its own
    # -- lives in a ref.
    elapsed, set_elapsed = use_state(0.0)
    running, set_running = use_state(False)
    started_at: Ref[float | None] = use_ref(None)

    async def tick():
        # The ref is the same object on every render, so this long-lived loop
        # always sees whatever the button handlers wrote into it.
        while started_at.current is not None:
            await asyncio.sleep(0.1)
            set_elapsed(time.monotonic() - started_at.current)

    use_async_effect(tick, dependencies=[running])

    def start(event=None):
        started_at.current = time.monotonic()
        set_running(True)

    def stop(event=None):
        if started_at.current is not None:
            # Write the exact final value; the ticking effect only samples
            # every 0.1s, so relying on it alone would leave the display
            # slightly behind.
            set_elapsed(time.monotonic() - started_at.current)
            started_at.current = None
        set_running(False)

    def reset(event=None):
        started_at.current = None
        set_running(False)
        set_elapsed(0.0)

    return html.div(
        html.h1(f"{elapsed:.1f}s"),
        html.button(
            {"onClick": stop if running else start},
            "Stop" if running else "Start",
        ),
        html.button({"onClick": reset}, "Reset"),
    )
1
# TODO

Writing from inside a lambda

box.current = value is an assignment, which is a statement, so it cannot be the body of a lambda. set_current exists for that case, and hands back what you replaced.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@component
def theme_picker():
    # `ref.current = value` is a statement, so it cannot appear inside the
    # `lambda` a one-line handler needs. `set_current` is the expression-shaped
    # version of the same write, and it hands back the value you replaced --
    # which is exactly what you need to remember the previous choice.
    previous = use_ref("light")
    theme, set_theme = use_state("light")

    def pick(name):
        return lambda event=None: (previous.set_current(theme), set_theme(name))

    return html.div(
        html.h1(f"{theme} theme"),
        html.p(f"you were just using: {previous.current}"),
        html.button({"onClick": pick("light")}, "Light"),
        html.button({"onClick": pick("dark")}, "Dark"),
    )
1
# TODO

pick returns a lambda precisely so each button gets its own handler, and previous.set_current(theme) is what makes the one-line body possible.


Troubleshooting

My ref value is stale

Check which of these you did:

  • You rendered the box, not its contents. html.p(my_ref) prints Ref(...). Use my_ref.current.
  • You expected the write to re-render. A ref never schedules a render. If the screen must change, that value is state.
  • You listed the ref as a dependency. Mutating current does not invalidate effects or memos — the box always compares equal to itself. Put the value in the dependency list, or use state.
  • You read a ref through an old closure. Inside a long-lived effect, re-read box.current on each pass instead of binding it to a local once.

My expensive setup runs on every render

You passed it as the argument: use_ref(build()) calls build() every render. Move it behind a guard:

box = use_ref(None)
if box.current is None:
    box.current = build()

I need to focus an input, or scroll to an element

There is no way to do this from a ReactPy server component, and no "ref_current" attribute will add one. Use the declarative attributes the browser already understands — "autoFocus", "checked", "value", "disabled" — and reach for a client-side app if you truly need imperative browser access.

Authors: Archmonger