Back to Reflex

Memo

docs/library/other/memo.md

0.9.911.4 KB
Original Source
python
import reflex as rx

Memo

The @rx.memo decorator turns a function into a memoized React component. The compiler emits the function as its own module, and React's memo only re-renders it when its declared props change. Reach for it when a subtree is expensive to render and depends on a narrow slice of state.

Requirements

Every parameter must be annotated with rx.Var[...] or rx.RestProp. The compiler reads those annotations to generate prop names, prop forwarding, and the JS function signature.

  1. rx.Var[T] for props — annotate each prop as rx.Var[T] where T is the prop's runtime type (str, int, a TypedDict, etc.). Inside the function body, the parameter is a Var you compose into the rendered tree.
  2. rx.RestProp for spread props — at most one parameter may be annotated as rx.RestProp, which forwards unrecognized kwargs through to the rendered root.
  3. rx.Var[rx.Component] for slot children — a parameter named children annotated as rx.Var[rx.Component] accepts children rendered by the caller.
  4. Keyword arguments at the call site — pass props by name, not by position.

Defaults need to be rx.Var values. For the common empty cases use the module-level constants rx.EMPTY_VAR_STR (an empty string), rx.EMPTY_VAR_INT (zero), and rx.EMPTY_VAR_COMPONENT (an empty component): class_name: rx.Var[str] = rx.EMPTY_VAR_STR falls back to "" when the caller omits the prop, and children: rx.Var[rx.Component] = rx.EMPTY_VAR_COMPONENT makes a slot optional.

Basic Usage

python
class DemoState(rx.State):
    count: int = 0

    @rx.event
    def increment(self):
        self.count += 1


@rx.memo
def expensive_component(label: rx.Var[str]) -> rx.Component:
    return rx.vstack(
        rx.heading(label, as_="h2"),
        rx.text("This component only re-renders when props change."),
        rx.divider(),
    )


def index():
    return rx.vstack(
        rx.text(f"Count: {DemoState.count}"),
        rx.button("Increment", on_click=DemoState.increment),
        expensive_component(label="Memoized Component"),
    )

expensive_component re-renders only when label changes — bumping DemoState.count does not invalidate it.

With State Variables

Props can be ordinary Vars. The memoized component re-renders when those Vars change:

python
class AppState(rx.State):
    name: str = "World"


@rx.memo
def greeting(name: rx.Var[str]) -> rx.Component:
    return rx.heading("Hello, " + name, as_="h2")


def index():
    return rx.vstack(
        greeting(name=AppState.name),
        rx.input(value=AppState.name, on_change=AppState.set_name),
    )

Binding state to a prop at the call site does not pull the state into the page. The compiler moves that call into a generated wrapper component that holds the state hooks the prop needs, so the page itself keeps no dependency on the state. When the state changes, the wrapper re-renders and React's memo stops there unless the prop's value actually changed. The page function itself never re-runs, so nothing in it re-renders except the components that read the changed state themselves — each inside its own wrapper, the rx.input above included.

That makes the call site the place to punch a single dependency through to an expensive component: pass exactly the Vars it needs, and it re-renders for those and nothing else, however much the rest of the state churns.

Using with rx.foreach

To render a memoized component for each item of a list Var, wrap the call in a lambda and pass the props by keyword. Also pass a key prop that uniquely identifies each item — React uses it to track items across re-renders. For the key to reach the rendered element, declare an rx.RestProp parameter and spread it onto the returned component; key flows through the ...rest spread and React consumes it. Do not declare key itself in the memo's signature.

python
from typing import TypedDict


class Task(TypedDict):
    id: str
    name: str


class TaskState(rx.State):
    tasks: list[Task] = [
        {"id": "1", "name": "Write docs"},
        {"id": "2", "name": "Review PR"},
    ]


@rx.memo
def task_card(rest: rx.RestProp, *, task: rx.Var[Task]) -> rx.Component:
    return rx.card(rx.text(task["name"]), rest)


def index():
    return rx.vstack(
        rx.foreach(
            TaskState.tasks,
            lambda task: task_card(task=task, key=task["id"]),
        ),
    )

Inside the memo body, task is a Var, not a plain dict: index into it with task["name"] or use it in f-strings, but do not iterate over it or call Python dict methods like .keys() — only Var operations are available.

Forwarding Props with rx.RestProp

Use rx.RestProp to accept and forward arbitrary props (think ...rest in JSX). Useful for thin wrappers that re-style a primitive without redeclaring every prop.

python
@rx.memo
def primary_button(
    rest: rx.RestProp,
    *,
    label: rx.Var[str],
) -> rx.Component:
    return rx.button(label, rest, class_name="bg-primary-9 text-white")


def index():
    return primary_button(
        label="Save",
        on_click=rx.console_log("clicked"),
        id="save",
    )

At most one rx.RestProp parameter is allowed per memo.

The rest parameter should be treated as an opaque value and passed positionally to any component which will use it.

You may use the .merge var operation to combine the arbitrary props with another object Var or python dict. The memo body can read placeholders like rest.get("class_name", ""), but the actual value will be unavailable at compile time, so you can't branch on it or do python operations with the values, only var operations which will be translated to Javascript expressions.

The same example as above, but now allowing the caller to optionally pass a class_name that gets merged with the default styles:

python
@rx.memo
def primary_button(
    rest: rx.RestProp,
    *,
    label: rx.Var[str],
) -> rx.Component:
    class_name = rest.get("class_name", "") + " bg-primary-9 text-white"
    return rx.button(label, rest.merge({"class_name": class_name}))

Limitation: props consumed at build time

rx.RestProp forwards props to the rendered element at runtime, so it can only carry props the element itself understands — real component props and CSS props. It cannot carry a prop that the target component's create() consumes at build time to decide what gets rendered.

The memo body runs once, when the app compiles — before any caller has passed a value. A value sent later through rest arrives only in the browser, after the target's create() has already run, so it never reaches that code. It is then emitted as a plain prop or as CSS and silently has no effect. Build-time props include is_external on rx.link (it selects a router link), the tag on rx.icon (it picks which icon to import), and any custom create() that consumes a keyword to reshape its output.

To forward such a prop, give it its own rx.Var[...] parameter and place it in the body yourself instead of routing it through rest:

python
class CustomText(rx.el.Span):
    @classmethod
    def create(cls, *children, prefix: rx.Var[str] | str = "", **props) -> rx.Component:
        return super().create(prefix, *children, **props)


# `prefix` is consumed by `create`, so it cannot arrive through `rest`.
# Declaring it as a parameter lets the memo body pass it to `create` itself.
@rx.memo
def styled_text(rest: rx.RestProp, *, prefix: rx.Var[str]) -> rx.Component:
    return CustomText.create("Foo", rest, prefix=prefix)


def index():
    return styled_text(prefix="P: ", class_name="c")

Accepting Children

Declare a parameter named children typed as rx.Var[rx.Component] to receive a child subtree.

python
@rx.memo
def card(
    children: rx.Var[rx.Component],
    *,
    title: rx.Var[str],
) -> rx.Component:
    return rx.box(
        rx.heading(title, as_="h2"),
        children,
        class_name="border border-secondary-5 rounded-lg p-4",
    )


def index():
    return card(
        rx.text("Body copy goes here."),
        title="Memoized card",
    )

Returning a Var Instead of a Component

A memo function can return rx.Var[T] instead of rx.Component. The compiler emits a plain JavaScript function and the call site is just a Var you can compose into the page.

python
class PriceState(rx.State):
    amount: int = 100
    currency: str = "USD"


@rx.memo
def format_price(amount: rx.Var[int], currency: rx.Var[str]) -> rx.Var[str]:
    return currency.to(str) + ": $" + amount.to(str)


def index():
    formatted = format_price(amount=PriceState.amount, currency=PriceState.currency)
    return rx.vstack(
        rx.text(formatted),
    )

The body of a Var-returning memo runs at compile time and is restricted to Var operations — no hooks, no Python branching on the Vars.

Customizing the JavaScript Wrapper

By default the compiled function component is wrapped in React's memo helper. Pass wrapper= to swap it for a different function — any Var whose JavaScript expression is callable, typically an rx.vars.FunctionStringVar carrying its own imports — or pass wrapper=None to export the bare function component with no wrapper at all.

python
observer = rx.vars.FunctionStringVar(
    "observer",
    _var_data=rx.vars.VarData(imports={"mobx-react-lite": "observer"}),
)


@rx.memo(wrapper=observer)
def observed_panel(label: rx.Var[str]) -> rx.Component:
    return rx.text(label)


@rx.memo(wrapper=None)
def custom_sankey_node(x: rx.Var[int], y: rx.Var[int]) -> rx.Component:
    return rx.box(width=x.to_string(), height=y.to_string())

The wrapper's imports ride along with the Var, so a custom wrapper brings its own import statement and wrapper=None pulls in nothing. wrapper= only applies to component-returning memos — a Var-returning memo compiles to a plain function and rejects the argument.

Performance Considerations

Reach for rx.memo when:

  • The component is expensive to render.
  • Its output is a stable function of a small set of props.
  • A frequently-updating ancestor would otherwise force it to re-render.

Skip it when:

  • The component is cheap and the bookkeeping is not worth it.
  • The props change on every render anyway — memo never gets to short-circuit.

Migrating from the Old rx.memo

The previous rx.memo accepted plain-typed arguments (def card(title: str)). The new one requires rx.Var[...]. To migrate:

python
# Before
@rx.memo
def card(title: str) -> rx.Component: ...


# After
@rx.memo
def card(title: rx.Var[str]) -> rx.Component: ...

The old rx._x.memo alias still resolves to the new memo and prints a one-time was promoted to rx.memo notice.

API Reference

rx.memo

python
rx.memo(component_fn)
rx.memo(wrapper=...)(component_fn)

Wraps a function whose parameters are all rx.Var[...] or rx.RestProp. Returns a callable that constructs the memoized component (or a Var if the function's return annotation is rx.Var[T]). Called with only keyword arguments, it returns a decorator applying them.

ArgumentTypeDescription
component_fnCallable[..., rx.Component | rx.Var]The function to memoize. All parameters must be rx.Var[...] or rx.RestProp.
wrapperrx.Var | NoneThe JS function the compiled function component is wrapped in. Defaults to React's memo; pass None to omit the wrapper. Only supported on component-returning memos.