Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
78 changes: 77 additions & 1 deletion docs/library/other/memo.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ 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.
The `@rx.memo` decorator emits a component or `rx.Var`-returning function in
its own module and memoizes it at runtime. React component memos re-render only
when their declared props change, while function memos reuse cached results for
unchanged arguments.

## Requirements

Expand Down Expand Up @@ -47,6 +50,79 @@ def index():

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

Use `name=` to override the generated memo name, which is useful for lambdas:

```python
named_memo = rx.memo(name="named_memo")(lambda label: rx.text(label))
```

The name must have valid JavaScript identifier characters. Reflex appends its
memo marker to explicit names so JavaScript keywords remain safe.

For props whose values are recreated but equal on each render, use
`by_value=True` to compare their serialized values:

```python
@rx.memo(by_value=True)
def settings_panel(settings: rx.Var[dict[str, str]]) -> rx.Component:
return rx.text(settings["title"])
```

If `wrapper=` is also supplied, the value-based memo wrapper is applied around
the custom wrapper rather than replacing it.

By default, an explicit memo is the auto-memoization boundary for its children.
Pass `recursive=True` when hook-bearing child components should also be
auto-memoized independently. This is useful when the explicit memo provides a
reusable module boundary but state reads should still re-render as close to
their use as possible:

```python
class MetricsState(rx.State):
request_count: int = 0


def live_request_count() -> rx.Component:
return rx.text("Requests: ", MetricsState.request_count)


@rx.memo(recursive=True)
def dashboard_shell() -> rx.Component:
return rx.card(
rx.heading("Dashboard"),
live_request_count(),
)


def analytics_page() -> rx.Component:
return dashboard_shell()


def admin_page() -> rx.Component:
return dashboard_shell()
```

`dashboard_shell` is emitted once as a reusable component module. Because it is
recursive, the state-bearing `live_request_count` subtree gets its own nearby
auto-memo boundary instead of making the whole dashboard shell depend directly
on `MetricsState.request_count`.

## Memoized Functions

A function returning `rx.Var[...]` is emitted as a JavaScript function and
memoizes its return value by argument identity. Repeated calls with the same
arguments reuse the cached result, including calls made while rendering:

```python
@rx.memo
def format_total(total: rx.Var[int]) -> rx.Var[str]:
return "$" + total.to(str)
```

Use `by_value=True` to key that cache by serialized argument values, or
`wrapper=None` to emit a plain uncached function. Custom wrappers work for
function memos as well. `recursive=True` only applies to component memos.

## With State Variables

Props can be ordinary Vars. The memoized component re-renders when those Vars change:
Expand Down
1 change: 1 addition & 0 deletions news/+memo-value-recursive.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Extend `@rx.memo` with `by_value=True` for component props and function arguments, `recursive=True` to auto-memoize reactive descendants, and `name=` to override generated names. Function memos now cache return values by argument identity by default; pass `wrapper=None` to emit a plain function.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Extend `@rx.memo` with `by_value=True` for component props and function arguments, `recursive=True` to auto-memoize reactive descendants, and `name=` to override generated names. Function memos now cache return values by argument identity by default; pass `wrapper=None` to emit a plain function.
30 changes: 23 additions & 7 deletions packages/reflex-base/src/reflex_base/compiler/templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -909,6 +909,23 @@ def dynamic_components_module_template(
_MEMO_WRAPPER_CALLEE_RE = re.compile(r"[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*")


def _apply_memo_wrapper(expression: str, wrapper: str | None) -> str:
"""Apply an optional memo wrapper to a JavaScript function expression.

Args:
expression: The function expression to wrap.
wrapper: The wrapper expression, or ``None`` for a bare function.

Returns:
The wrapped JavaScript expression.
"""
if not wrapper:
return expression
if not _MEMO_WRAPPER_CALLEE_RE.fullmatch(wrapper):
wrapper = f"({wrapper})"
return f"{wrapper}{expression}"


def _render_memo_component(component: dict[str, Any]) -> str:
"""Render the ``export const`` statement for one memoized component.

Expand All @@ -932,10 +949,7 @@ def _render_memo_component(component: dict[str, Any]) -> str:
{_RenderUtils.render(component["render"])}
)
}})"""
wrapper = component.get("wrapper")
if wrapper and not _MEMO_WRAPPER_CALLEE_RE.fullmatch(wrapper):
wrapper = f"({wrapper})"
export_expr = f"{wrapper}{function_expr}" if wrapper else function_expr
export_expr = _apply_memo_wrapper(function_expr, component.get("wrapper"))
name = component["name"]
# ``display_name`` is resolved by the caller (``compile_experimental_component_memo``),
# which is the layer that knows the memo's clean export name — the JS symbol
Expand Down Expand Up @@ -974,9 +988,10 @@ def memo_components_template(

functions_code = ""
for function in functions:
functions_code += (
f"\nexport const {function['name']} = {function['function']};\n"
function_expr = _apply_memo_wrapper(
function["function"], function.get("wrapper")
)
functions_code += f"\nexport const {function['name']} = {function_expr};\n"

return f"""
{imports_str}
Expand Down Expand Up @@ -1037,10 +1052,11 @@ def memo_single_function_template(
The rendered standalone function memo module code.
"""
imports_str = "\n".join([_RenderUtils.get_import(imp) for imp in imports])
function_expr = _apply_memo_wrapper(function["function"], function.get("wrapper"))
return f"""
{imports_str}

export const {function["name"]} = {function["function"]};
export const {function["name"]} = {function_expr};
"""


Expand Down
Loading
Loading