Skip to content

Use Memo

Overview

use_memo is a ReactPy hook that caches the result of a calculation, recomputing it only when one of its listed dependencies changes.

cached_value = use_memo(calculate_value, dependencies=[dep_1, dep_2])

Use it when a calculation is genuinely expensive and the inputs it depends on change less often than the component renders. It caches a value; it does not stop anything from rendering.

What use_memo does not do in ReactPy

It does not prevent a child component from re-rendering. ReactPy has no render bailout and no memo: a child's body runs whenever its parent re-renders, even with identical props, and even if you cache the child element itself with use_memo. Texts ported from React that use useMemo to "skip re-rendering of components" describe a mechanism that is not here. use_memo saves the computation, nothing else.


Reference

use_memo(function, dependencies)

Call use_memo at the top level of your component to cache a calculation.

from reactpy import use_memo

Parameters

  • function: A function taking no arguments that returns the value you want. ReactPy calls it only when it must. It must be pure: same inputs, same output, no side effects.
  • dependencies: The values the calculation reads. ReactPy recomputes when any of them changes. It accepts three shapes:

    You pass Meaning
    dependencies=[a, b] Recompute when a or b changes.
    omitted (the default) ReactPy reads the function's closure and infers the free variables.
    dependencies=None Recompute on every render.

Returns

The current value — either freshly computed, or the one cached from the last render in which a dependency changed.

Caveats

  • The decorated name holds the value, not a function. The decorator form is the call form under another name, and the call form returns the value -- so after decorating, my_value is the cached result, and calling it raises TypeError: 'int' object is not callable. To keep the name callable, give the inner function its own name and pass it to use_memo.
  • Caching lasts for the lifetime of the component. A cached value survives as long as the component stays mounted; there is no way to invalidate it from inside. Mounting the component under a new "key" throws it away, because that unmounts and remounts the component and every hook starts over.
  • You must list every value the function reads. ReactPy does not check, and does not warn. A missing dependency produces a silently stale value, not an error. See I removed a dependency and nothing complained.
  • Comparison is not plain identity. Dependencies go through reactpy.core.hooks.strictly_equal, which requires the two values to be the same type first, compares two locally-defined functions or lambdas by their code objects (so a fresh, structurally identical lambda counts as unchanged), and otherwise compares with ==, falling back to is only if that raises. Two practical consequences: a freshly built list or dict with equal contents does not force a recompute, and a value that changes type between renders — 1 then 1.0, or 0 then False — always counts as changed.
  • Mutating a dependency in place defeats the cache. Change an object and pass the same object back, and it compares equal to itself, so the cached value is kept. Build a new object instead — see Updating Objects in State.
  • A ref never invalidates a memo. Writing to some_ref.current does not change how the ref compares, so listing a use_ref box in dependencies accomplishes nothing.
  • 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; a hook that runs on some renders but not others produces a scrambled component and a cryptic IndexError.

Usage

Skipping an expensive calculation

Give use_memo the function that does the work, and list what it reads. The leaderboard sorts seven rows here, which is nothing — but it is the same shape you would use for a sort over ten thousand, and the counter on screen shows exactly how often the sort runs.

Press Change highlight: the component re-renders, the sort does not run, and sorted 1 time(s) stays put. Press Everyone scores +1: players changes, so the sort runs again.

 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
45
46
47
48
49
50
51
ROSTER = [
    ("ada", 91),
    ("grace", 120),
    ("alan", 77),
    ("edsger", 64),
    ("katherine", 143),
    ("dorothy", 88),
    ("margaret", 155),
]

sorts = []  # demo-only: lets you *see* how often `rank` actually runs


@component
def leaderboard():
    players, set_players = use_state(
        [{"name": name, "score": score} for name, score in ROSTER]
    )
    highlight, set_highlight = use_state("ada")

    def rank():
        # The work you want to skip: sorting the whole table. Listing
        # `dependencies=[players]` runs this only when the roster changes, so
        # changing the highlight leaves the sorted list alone.
        sorts.append(1)
        return sorted(players, key=lambda player: -player["score"])

    ranked = use_memo(rank, dependencies=[players])

    return html.div(
        html.h1("Leaderboard"),
        html.ol([html.li(f"{player['name']}: {player['score']}") for player in ranked]),
        html.p(f"sorted {len(sorts)} time(s)"),
        html.p(f"highlighted: {highlight}"),
        html.button(
            {
                "onClick": lambda event: set_highlight(
                    "grace" if highlight == "ada" else "ada"
                )
            },
            "Change highlight",
        ),
        html.button(
            {
                "onClick": lambda event: set_players(
                    [{**player, "score": player["score"] + 1} for player in players]
                )
            },
            "Everyone scores +1",
        ),
    )
1
# TODO

The counter (sorts) and both buttons exist only to make the caching observable. In real code you would keep the use_memo call and drop the rest.

How expensive is "expensive"?

Enough to measure. Sorting a small list, building a dict from a handful of items, or formatting a string is not worth it — the hook has a cost of its own, and a dependency list you must keep correct is a cost too.

Reach for it when you recognize one of these:

  • A sort, filter, or search over a large collection.
  • Parsing: JSON, CSV, a template, a source file.
  • Building a structure that then gets walked repeatedly.
  • Anything you have already timed and found annoying.

If you have not measured it, prefer the plain calculation. Adding use_memo later is a one-line change precisely because the calculation is already in its own function.

Should you add use_memo everywhere?

No. Each one is a dependency list you now have to maintain correctly, and a wrong list is a silent bug rather than a loud one. Memoize where you have measured, not everywhere.

Omitting the dependency list

Leave out dependencies and ReactPy infers them from the free variables in the function's closure. This is a ReactPy feature with no React equivalent, and it removes the most common class of mistake, because a value the function reads is by definition a free variable.

The word count below reads text but not upper, so toggling the case re-renders the component without re-running the count. Watch counted 1 time(s).

 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
counts = []  # demo-only: lets you *see* how often `count` actually runs


@component
def word_count():
    text, set_text = use_state("reactpy is a python ui library")
    upper, set_upper = use_state(False)

    # No `dependencies` argument at all. ReactPy inspects the function's
    # closure and infers the free variables it reads -- here that is `text`,
    # but not `upper`, so toggling the case does not redo the count.
    def count():
        counts.append(1)
        return len(text.split())

    total = use_memo(count)

    return html.div(
        html.h1(f"{total} words"),
        html.p(text.upper() if upper else text),
        html.p(f"counted {len(counts)} time(s)"),
        html.button({"onClick": lambda event: set_upper(not upper)}, "Toggle case"),
        html.input(
            {
                "value": text,
                "onChange": lambda event: set_text(event["target"]["value"]),
            }
        ),
    )
1
# TODO

Two catches. Inference only sees closure variables, so a module-level name, or anything reached through an object, is invisible to it. And inference is a convenience for a function defined inside the component — pass the list explicitly when the function comes from elsewhere.

Recomputing on every render

dependencies=None opts out of caching entirely: the function runs on every render. That is what you want when the calculation reads something no dependency list can express, such as a module-level mutable.

The label below reads config, which lives at module scope. Inferred dependencies would be empty, so the label would be computed once and never notice a changed config. With dependencies=None, label() runs each render.

 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
# Module-level, so it is *not* a closure variable and ReactPy cannot infer it
# as a dependency.
config = {"prefix": "count"}

calls = []  # demo-only


@component
def counter():
    count, set_count = use_state(0)
    suffix, set_suffix = use_state("")

    def label():
        # Reads `config`, which ReactPy cannot see as a dependency. With
        # inferred (i.e. empty) dependencies this would be computed once and
        # then never notice an edited `config`. `dependencies=None` says: this
        # depends on the render itself, so recompute every time.
        calls.append(1)
        return f"{config['prefix']}: {count}{suffix}"

    return html.div(
        html.h1(use_memo(label, dependencies=None)),
        html.p(f"label() ran {len(calls)} time(s)"),
        html.button({"onClick": lambda event: set_count(count + 1)}, "Count +1"),
        html.button({"onClick": lambda event: set_suffix("!")}, "Set suffix"),
    )
1
# TODO

Troubleshooting

My memo value never updates

In order of likelihood:

  • You mutated a dependency instead of replacing it. Same object in, equal to itself, cached value kept. Build a new object.
  • You listed a ref as a dependency. Mutating some_ref.current never invalidates anything. Use state, or list the value itself.
  • The dependency compares equal to its replacement. Two [1, 2, 3] lists are equal, so rebuilding one each render changes nothing.
  • You left the value out of the list. See the next item.

I removed a dependency and nothing complained

Because nothing checks. The example below omits tax_rate from the list, and it seems to work: press Add 10% tax twice and the rate climbs while the total stays frozen at whatever a 0% rate produced. Then press Add an item — a real dependency — and the total jumps to 12.00, silently incorporating both tax increases you could see never happened.

 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
@component
def totals():
    items, set_items = use_state([1, 2, 3])
    tax_rate, set_tax_rate = use_state(0.0)

    # `tax_rate` is read here but deliberately left out of `dependencies`.
    # ReactPy does not complain. The cached total just keeps the rate from the
    # render that last computed it, so "Add 10% tax" appears to do nothing --
    # until something that *is* a dependency changes, and the stale number
    # jumps to the right one all at once.
    def grand_total():
        return sum(items) * (1 + tax_rate)

    total = use_memo(grand_total, dependencies=[items])

    return html.div(
        html.h1(f"total: {total:.2f}"),
        html.p(f"tax rate: {tax_rate:.0%}"),
        html.button(
            {"onClick": lambda event: set_tax_rate(tax_rate + 0.1)}, "Add 10% tax"
        ),
        html.button(
            {"onClick": lambda event: set_items([*items, len(items) + 1])},
            "Add an item",
        ),
    )
1
# TODO

This is the failure mode to watch for: a stale value that eventually arrives, so it reads as lag rather than as a bug. List everything the function reads, or omit dependencies and let ReactPy infer them.

My calculation runs on every render anyway

  • You passed dependencies=None.
  • A dependency really is new each render — commonly an object constructed in the component body above the use_memo call.
  • A dependency is an object whose == is identity-based and which you rebuild each time.

I wanted to stop a child from re-rendering

use_memo cannot do this in ReactPy, and caching the child element does not help either — a cached element is still re-rendered. ReactPy re-renders children unconditionally. If a child is genuinely expensive, the levers are to make its own calculations cheap (memoize inside it) or to avoid rendering it at all behind a conditional.

Authors: Archmonger