Skip to content

Referencing Values with Refs

Overview

When you want a component to "remember" some information, but you don't want that information to trigger new renders, you can use a ref.

You will learn

  • How to add a ref to your component
  • How to update a ref's value
  • How refs are different from state
  • How to use refs safely

Adding a ref to your component

You can add a ref to your component by importing the use_ref hook from ReactPy:

from reactpy import use_ref

Inside your component, call the use_ref hook and pass the initial value that you want to reference as the only argument. For example, here is a ref to the value 0:

clicks = use_ref(0)

use_ref returns a Ref object holding that value:

Ref(0)  # holds the value you passed to use_ref

You can access the current value of that ref through the clicks.current attribute. This value is intentionally mutable, meaning you can both read and write to it. It's like a secret pocket of your component that ReactPy doesn't track. (This is what makes it an "escape hatch" from ReactPy's one-way data flow — more on that below!)

Here, a button will increment clicks.current on every click:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@component
def counter():
    clicks = use_ref(0)

    def handle_click(event):
        clicks.current += 1
        # Nothing on screen changes, but the server console proves the ref is
        # really incrementing between clicks.
        print(f"You clicked {clicks.current} times!")

    return html.button({"onClick": handle_click}, "Click me!")
1
# TODO

The ref points to a number, but, like state, you could point to anything: a string, an object, or even a function. Unlike state, a ref is a plain Python object with a current attribute that you can read and modify.

Note that the component doesn't re-render with every increment. Like state, refs are retained by ReactPy between re-renders. However, setting state re-renders a component. Changing a ref does not! Watch the server console: each click prints a new count, yet the page never redraws.

Pass the initial value

Unlike useRef() in React, use_ref() has no no-argument form — initial_value is required, so use_ref() fails outright with TypeError: use_ref() missing 1 required positional argument: 'initial_value'. Pass None explicitly when you have no better starting value: tick_task = use_ref(None).

The lower-level reactpy.utils.Ref() is the one that accepts no arguments, and there current starts unset rather than at None, so the first read raises AttributeError: 'Ref' object has no attribute 'current'. Components should call use_ref rather than construct a Ref directly.

Example: building a stopwatch

You can combine refs and state in a single component. For example, let's make a stopwatch that the user can start or stop by pressing a button. In order to display how much time has passed since the user pressed "Start", you will need to keep track of when the Start button was pressed and what the current time is. This information is used for rendering, so you'll keep it in state:

elapsed, set_elapsed = use_state(0.0)
running, set_running = use_state(False)

When the user presses "Start", you'll update the elapsed time about ten times a second. ReactPy has no setInterval, but it runs on asyncio, so the equivalent is a coroutine that sleeps and sets state, started with use_async_effect.

Now the awkward part. That coroutine runs for a long time, and it needs to know when the user pressed Start in order to compute time.monotonic() - <start>. It cannot read it from a local variable, because the render that created the coroutine is long gone by the time the coroutine wakes up. Since the start time is not used for rendering directly, you can keep it in a ref:

 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

Press Start and the counter climbs; press Stop and it freezes at the exact final value. The started_at ref is the same object on every render, so the button handlers and the long-lived ticking coroutine all see whatever the others wrote into it.

Note what the ref is not doing here: it is not holding the asyncio.Task. The ticking coroutine is owned by use_async_effect, which cancels it when running flips to False and when the component unmounts. Storing a manually created task in a ref is tempting — tick_task.current = asyncio.create_task(keep_ticking()) — but nothing ever cancels it. Measure it by unmounting the component while it runs and the loop keeps rendering forever. Reach for an effect to own background work, and use the ref for the plain values the handlers need to share.

When a piece of information is used for rendering, keep it in state. When a piece of information is only needed by event handlers and changing it doesn't require a re-render, using a ref may be more efficient.

Differences between refs and state

Perhaps you're thinking refs seem less "strict" than state — you can mutate them instead of always having to use a state setting function, for instance. But in most cases, you'll want to use state. Refs are an "escape hatch" you won't need often. Here's how state and refs compare:

refs state
use_ref(initial_value) returns Ref(current=initial_value) use_state(initial_value) returns the current value of a state variable and a state setter function
Doesn't trigger re-render when you change it. Triggers re-render when you change it.
Mutable — you can modify and update current outside of the rendering process. "Immutable" — you must use the state setting function to modify state variables to queue a re-render.
You shouldn't read (or write) current during rendering. You can read state at any time. However, each render has its own snapshot of state which does not change.

Here is a counter button that's implemented with state:

1
2
3
4
5
6
7
8
@component
def counter():
    count, set_count = use_state(0)

    def handle_click(event):
        set_count(count + 1)

    return html.button({"onClick": handle_click}, f"You clicked {count} times")
1
# TODO

Because the count value is displayed, it makes sense to use a state value for it. When the counter's value is set with set_count(), ReactPy re-renders the component and the screen updates to reflect the new count.

If you tried to implement this with a ref, ReactPy would never re-render the component, so you'd never see the count change! See how clicking this button does not update its text:

1
2
3
4
5
6
7
8
9
@component
def counter():
    count = use_ref(0)

    def handle_click(event):
        # This doesn't re-render the component!
        count.current += 1

    return html.button({"onClick": handle_click}, f"You clicked {count.current} times")
1
# TODO

This is why reading ref.current during render leads to unreliable code. If you need that, use state instead.

How does use_ref work inside?

Although both use_state and use_ref are provided by ReactPy, use_ref really is implemented on top of use_state. In reactpy/core/hooks.py:

def use_ref(initial_value):
    return _use_const(lambda: Ref(initial_value))


def _use_const(function):
    return HOOK_STACK.current_hook().use_state(function)

During the first render, use_ref stores a Ref object as if it were state. That object is retained by ReactPy, so every later render gets the very same object back. There is no setter in the picture because use_ref always needs to return the same object — which is exactly why it can reuse state machinery without re-rendering.

If you're familiar with object-oriented programming, refs might remind you of instance fields — but instead of self.something you write something.current.

When to use refs

Typically, you will use a ref when your component needs to "step outside" ReactPy and communicate with external APIs — often an API that won't impact the appearance of the component. Here are a few of these rare situations:

  • Storing asyncio tasks or timer handles, as in the stopwatch above
  • Storing and manipulating DOM elements, which we cover on the next page
  • Storing other objects that aren't needed to calculate the VDOM

If your component needs to store some value, but it doesn't impact the rendering logic, choose refs.

Best practices for refs

Following these principles will make your components more predictable:

  • Treat refs as an escape hatch. Refs are useful when you work with external systems. If much of your application logic and data flow relies on refs, you might want to rethink your approach.
  • Don't read or write ref.current during rendering. If some information is needed during rendering, use state instead. Since ReactPy doesn't know when ref.current changes, even reading it while rendering makes your component's behavior difficult to predict. (The only exception to this is code like if not ref.current: ref.current = Thing(), which only sets the ref once during the first render.)

Limitations of ReactPy state don't apply to refs. For example, state acts like a snapshot for every render and doesn't update synchronously. But when you mutate the current value of a ref, it changes immediately:

clicks.current = 5
print(clicks.current)  # 5

This is because the ref itself is a regular Python object, and so it behaves like one.

You also don't need to worry about avoiding mutation when you work with a ref. As long as the object you're mutating isn't used for rendering, ReactPy doesn't care what you do with the ref or its contents.

Refs and the DOM

You can point a ref to any value. However, the most common use case for a ref is to access a DOM element — for example, if you want to focus an input programmatically. ReactPy does not have a ref attribute you can pass to a tag; instead you hand the element an id and reach it from an effect. You can read more about this in Manipulating the DOM with Refs.

Summary

  • A ref is a value that lets a component "remember" something without triggering a re-render.
  • Call use_ref(initial_value) and read or write ref.current.
  • Setting state re-renders; changing a ref does not, which makes refs suitable for values only event handlers need.
  • Don't read or write ref.current during rendering — use state for anything that affects what's on screen.
Authors: Archmonger