Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
b46f2e2
refactor: promote memo out of experimental into rx.memo
FarhanAliRaza May 13, 2026
8cc9b19
feat(memo): support rx.EventHandler params in component memos
FarhanAliRaza May 15, 2026
3bf899b
chore(memo): annotate rx.memo return types and tighten arrow_svg sig
FarhanAliRaza May 15, 2026
b4bba68
Merge remote-tracking branch 'upstream/main' into non-exp-memo
FarhanAliRaza May 15, 2026
6cb4b9b
chore(memo): re-add experimental memo shim as deprecated alias
FarhanAliRaza May 15, 2026
e6278b2
refactor(memo): drop Experimental prefix from memo registry and types
FarhanAliRaza May 15, 2026
0afce5c
spellfix
FarhanAliRaza May 15, 2026
d5650cf
feat(vars): expose EMPTY_VAR_STR and EMPTY_VAR_INT as memo-friendly d…
FarhanAliRaza May 15, 2026
c705546
chore(memo): tidy compile_app splats and inline-import cleanup
FarhanAliRaza May 18, 2026
67759cf
feat(memo): support self-referencing memos via name binding during eval
FarhanAliRaza May 20, 2026
76b8120
Merge remote-tracking branch 'upstream/main' into non-exp-memo
FarhanAliRaza May 20, 2026
11fe7f5
chore: update pyi hash for experimental memo
FarhanAliRaza May 20, 2026
805a049
feat(memo): soft-deprecate missing rx.memo annotations instead of err…
FarhanAliRaza May 20, 2026
b441d2f
feat(memo): suggest concrete return type in soft-deprecation warning
FarhanAliRaza May 20, 2026
ee9d1ed
chore: update pyi hash for experimental memo
FarhanAliRaza May 20, 2026
f751c74
refactor(memo): drop empty $/utils/components index file
FarhanAliRaza May 20, 2026
8f6cd8a
fix(memo): drop $/utils/components from window bundle
FarhanAliRaza May 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/app/reflex_docs/templates/docpage/docpage.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ def feedback_button_toc() -> rx.Component:


@rx.memo
def copy_to_markdown(text: str) -> rx.Component:
def copy_to_markdown(text: rx.Var[str]) -> rx.Component:
copied = ClientStateVar.create("is_copied", default=False, global_ref=False)
return marketing_button(
rx.cond(
Expand Down Expand Up @@ -297,7 +297,7 @@ def link_pill(text: str, href: str) -> rx.Component:


@rx.memo
def docpage_footer(path: str):
def docpage_footer(path: rx.Var[str]) -> rx.Component:
from reflex_site_shared.constants import FORUM_URL, ROADMAP_URL

return rx.el.footer(
Expand Down
8 changes: 4 additions & 4 deletions docs/enterprise/drag-and-drop.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class BasicDndState(rx.State):


@rx.memo
def draggable_card():
def draggable_card() -> rxe.dnd.Draggable:
return rxe.dnd.draggable(
rx.card(
rx.text("Drag me!", weight="bold"),
Expand Down Expand Up @@ -95,7 +95,7 @@ class MultiPositionState(rx.State):


@rx.memo
def movable_card():
def movable_card() -> rxe.dnd.Draggable:
return rxe.dnd.draggable(
rx.card(
rx.text("Movable Card", weight="bold"),
Expand Down Expand Up @@ -166,7 +166,7 @@ class StateTrackingState(rx.State):


@rx.memo
def tracked_draggable():
def tracked_draggable() -> rxe.dnd.Draggable:
drag_params = rxe.dnd.Draggable.collected_params
return rxe.dnd.draggable(
rx.card(
Expand Down Expand Up @@ -274,7 +274,7 @@ class DynamicListState(rx.State):


@rx.memo
def draggable_list_item(item: ListItem):
def draggable_list_item(item: rx.Var[ListItem]) -> rx.Component:
return rxe.dnd.draggable(
rx.card(
rx.text(item.text, weight="bold"),
Expand Down
2 changes: 1 addition & 1 deletion docs/enterprise/react_flow/edges.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ def button_edge(
sourcePosition: rx.Var[Position],
targetPosition: rx.Var[Position],
markerEnd: rx.Var[str],
):
) -> rx.Fragment:
bezier_path = rxe.components.flow.util.get_bezier_path(
source_x=sourceX,
source_y=sourceY,
Expand Down
4 changes: 2 additions & 2 deletions docs/enterprise/react_flow/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ class ConnectionLimitState(rx.State):
@rx.memo
def custom_handle(
type: rx.Var[HandleType], position: rx.Var[Position], connection_count: rx.Var[int]
):
) -> rxe.components.flow.Handle:
connections = rxe.flow.api.get_node_connections()
return rxe.flow.handle(
type=type,
Expand All @@ -190,7 +190,7 @@ def custom_handle(


@rx.memo
def custom_node():
def custom_node() -> rx.el.Div:
return rx.el.div(
custom_handle(type="target", position="left", connection_count=1),
rx.el.div("← Only one edge allowed"),
Expand Down
4 changes: 3 additions & 1 deletion docs/enterprise/react_flow/nodes.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,9 @@ class CustomNodeState(rx.State):


@rx.memo
def color_selector_node(data: rx.Var[dict], isConnectable: rx.Var[bool]):
def color_selector_node(
data: rx.Var[dict], isConnectable: rx.Var[bool]
) -> rx.Component:
data = data.to(dict)
return rx.el.div(
rxe.flow.handle(
Expand Down
2 changes: 1 addition & 1 deletion docs/library/data-display/icon.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ icon_search_cs = ClientStateVar.create("icon_search", default="")


@rx.memo
def lucide_icons():
def lucide_icons() -> rx.Component:
return rx.box(
rx.box(
rx.box(
Expand Down
187 changes: 93 additions & 94 deletions docs/library/other/memo.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,22 @@ import reflex as rx

# Memo

The `memo` decorator is used to optimize component rendering by memoizing components that don't need to be re-rendered. This is particularly useful for expensive components that depend on specific props and don't need to be re-rendered when other state changes in your application.
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

When using `rx.memo`, you must follow these 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. **Type all arguments**: All arguments to a memoized component must have type annotations.
2. **Use keyword arguments**: When calling a memoized component, you must use keyword arguments (not positional arguments).
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.

## Basic Usage
Defaults need to be `rx.Var` values. For the common empty cases use the module-level constants `rx.EMPTY_VAR_STR` (an empty string) and `rx.EMPTY_VAR_INT` (zero): `class_name: rx.Var[str] = rx.EMPTY_VAR_STR` falls back to `""` when the caller omits the prop.

When you wrap a component function with `@rx.memo`, the component will only re-render when its props change. This helps improve performance by preventing unnecessary re-renders.
## Basic Usage

```python
# Define a state class to track count
class DemoState(rx.State):
count: int = 0

Expand All @@ -27,150 +28,148 @@ class DemoState(rx.State):
self.count += 1


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


# Use the memoized component in your app
def index():
return rx.vstack(
rx.heading("Memo Example"),
rx.text("Count: 0"), # This will update with state.count
rx.text(f"Count: {DemoState.count}"),
rx.button("Increment", on_click=DemoState.increment),
rx.divider(),
expensive_component(label="Memoized Component"), # Must use keyword arguments
spacing="4",
padding="4",
border_radius="md",
border="1px solid #eaeaea",
expensive_component(label="Memoized Component"),
)
```

In this example, the `expensive_component` will only re-render when the `label` prop changes, not when the `count` state changes.
`expensive_component` re-renders only when `label` changes — bumping `DemoState.count` does not invalidate it.

## With Event Handlers
## With State Variables

You can also use `rx.memo` with components that have event handlers:
Props can be ordinary Vars. The memoized component re-renders when those Vars change:

```python
# Define a state class to track clicks
class ButtonState(rx.State):
clicks: int = 0

@rx.event
def increment(self):
self.clicks += 1
class AppState(rx.State):
name: str = "World"


# Define a memoized button component
@rx.memo
def my_button(text: str, on_click: rx.EventHandler) -> rx.Component:
return rx.button(text, on_click=on_click)
def greeting(name: rx.Var[str]) -> rx.Component:
return rx.heading("Hello, " + name)


# Use the memoized button in your app
def index():
return rx.vstack(
rx.text("Clicks: 0"), # This will update with state.clicks
my_button(text="Click me", on_click=ButtonState.increment),
spacing="4",
greeting(name=AppState.name),
rx.input(value=AppState.name, on_change=AppState.set_name),
)
```

## With State Variables
## Forwarding Props with `rx.RestProp`

When used with state variables, memoized components will only re-render when the specific state variables they depend on change:
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
# Define a state class with multiple variables
class AppState(rx.State):
name: str = "World"
count: int = 0
@rx.memo
def primary_button(
rest: rx.RestProp,
*,
label: rx.Var[str],
) -> rx.Component:
return rx.button(label, class_name="bg-primary-9 text-white", **rest)

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

@rx.event
def set_name(self, name: str):
self.name = name
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.

# Define a memoized greeting component
## Accepting Children

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

```python
@rx.memo
def greeting(name: str) -> rx.Component:
return rx.heading("Hello, " + name) # Will display the name prop
def card(
children: rx.Var[rx.Component],
*,
title: rx.Var[str],
) -> rx.Component:
return rx.box(
rx.heading(title),
children,
class_name="border border-slate-5 rounded-lg p-4",
)


# Use the memoized component with state variables
def index():
return rx.vstack(
greeting(name=AppState.name), # Must use keyword arguments
rx.text("Count: 0"), # Will display the count
rx.button("Increment Count", on_click=AppState.increment),
rx.input(
placeholder="Enter your name",
on_change=AppState.set_name,
value="World", # Will be bound to AppState.name
),
spacing="4",
return card(
rx.text("Body copy goes here."),
title="Memoized card",
)
```

## Advanced Event Handler Example
## Returning a `Var` Instead of a Component

You can also pass arguments to event handlers in memoized components:
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
# Define a state class to track messages
class MessageState(rx.State):
message: str = ""

@rx.event
def set_message(self, text: str):
self.message = text
class PriceState(rx.State):
amount: int = 100
currency: str = "USD"


# Define a memoized component with event handlers that pass arguments
@rx.memo
def action_buttons(
on_action: rx.EventHandler[rx.event.passthrough_event_spec(str)],
) -> rx.Component:
return rx.hstack(
rx.button("Save", on_click=on_action("Saved!")),
rx.button("Delete", on_click=on_action("Deleted!")),
rx.button("Cancel", on_click=on_action("Cancelled!")),
spacing="2",
)
def format_price(amount: rx.Var[int], currency: rx.Var[str]) -> rx.Var[str]:
return currency.to(str) + ": $" + amount.to(str)


# Use the memoized component with event handlers
def index():
formatted = format_price(amount=PriceState.amount, currency=PriceState.currency)
return rx.vstack(
rx.text("Status: "), # Will display the message
action_buttons(on_action=MessageState.set_message),
spacing="4",
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.

## Performance Considerations

Use `rx.memo` for:
Reach for `rx.memo` when:

- Components with expensive rendering logic
- Components that render the same result given the same props
- Components that re-render too often due to parent component updates
- 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.

Avoid using `rx.memo` for:
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: ...
```

- Simple components where the memoization overhead might exceed the performance gain
- Components that almost always receive different props on re-render
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

Expand All @@ -180,8 +179,8 @@ Avoid using `rx.memo` for:
rx.memo(component_fn)
```

Decorates a function that returns a Reflex component so it can be reused as a memoized component. The function arguments must be type annotated, and memoized components should be called with keyword arguments.
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]`).

| Argument | Type | Description |
| --- | --- | --- |
| `component_fn` | `Callable[..., rx.Component]` | Function that returns the component to memoize. |
| `component_fn` | `Callable[..., rx.Component \| rx.Var]` | The function to memoize. All parameters must be `rx.Var[...]` or `rx.RestProp`. |
16 changes: 0 additions & 16 deletions packages/reflex-base/src/reflex_base/compiler/templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -785,22 +785,6 @@ def memo_single_function_template(
"""


def memo_index_template(reexports: Iterable[tuple[str, str]]) -> str:
"""Template for the memo index module that re-exports every memo file.

Args:
reexports: Iterable of ``(export_name, relative_module_specifier)``.

Returns:
The rendered index module code.
"""
lines = [
f'export {{ {export_name} }} from "{specifier}";'
for export_name, specifier in reexports
]
return "\n".join(lines) + "\n"


def styles_template(stylesheets: list[str]) -> str:
"""Template for styles.css.

Expand Down
Loading
Loading