The Component Workshop: Reflex's 100 Library Components
·
Course overview
Layout
·
Box
·
Flex
·
Stack
·
Grid
·
Container
·
Section
·
Center
·
Spacer
·
Separator
·
Card
·
Inset
·
Aspect Ratio
·
Fragment
Typography
·
Text
·
Heading
·
Link
·
Code
·
Markdown
·
Blockquote
·
Quote
·
Strong
·
Em
·
Kbd
Forms
·
Button
·
Input
·
Text Area
·
Checkbox
·
Switch
·
Radio Group
·
Select
·
Slider
·
Form
·
Upload
Data display
·
Badge
·
Avatar
·
Icon
·
Callout
·
List
·
Data List
·
Code Block
·
Progress
·
Spinner
·
Scroll Area
·
Moment
Disclosure
·
Tabs
·
Accordion
·
Segmented Control
Overlay
·
Tooltip
·
Popover
·
Hover Card
·
Dialog
·
Alert Dialog
·
Drawer
·
Dropdown Menu
·
Context Menu
·
Toast
Dynamic rendering
·
Cond
·
Match
·
Foreach
·
Auto Scroll
Media
·
Image
·
Audio
·
Video
Tables and data grids
·
Table
·
Data Table
·
Data Editor
Charts
·
Area Chart
·
Bar Chart
·
Line Chart
·
Composed Chart
·
Pie Chart
·
Radar Chart
·
Radial Bar Chart
·
Scatter Chart
·
Funnel Chart
·
Treemap
·
Sankey Chart
·
Error Bar
Chart parts
·
Axis
·
Cartesian Grid
·
Chart Tooltip
·
Legend
·
Label
·
Reference
·
Brush
Other chart libraries
·
Plotly
·
Pyplot
HTML elements
·
HTML
·
HTML Text
·
HTML Layout
·
HTML Forms
·
HTML Media
·
HTML Tables
·
SVG
Other tools
·
Theme
·
Skeleton
·
Clipboard
·
Html Embed
·
Script
·
Memo
Workshop order
Split off the product cards so that the card list next to the cart doesn't re-render every time the cart count changes.
@rx.memo turns a function into a memoized React component. The compiler emits the function as a separate module, and React's memo re-renders it only when its declared props change.
Use it for subtrees that are expensive to render and depend on only a narrow slice of the state. All the @rx.memo functions on this page are defined above the example code (in the State source).
Count: 0
This component only re-renders when props change.
class MemoDemoState(rx.State):
count: int = 0
@rx.event
def increment(self):
self.count += 1
@rx.memo
def memo_expensive_component(label: rx.Var[str]) -> rx.Component:
return rx.vstack(
rx.heading(label, as_="h2", size="4"),
rx.text("This component only re-renders when props change."),
rx.divider(),
)
rx.vstack(
rx.text("Count: ", MemoDemoState.count),
rx.button("Increment", on_click=MemoDemoState.increment),
memo_expensive_component(label="Memoized Component"),
)memo_expensive_component re-renders only when label changes. Raising count with Increment doesn't re-render this component.
rx.memo has no visible props, so instead of a prop lab, here are examples driven by State. You can pass State Vars directly as props, and the component re-renders when that Var changes.
class MemoGreetState(rx.State):
name: str = "World"
@rx.event
def change_name(self, value: str):
self.name = value
@rx.memo
def memo_greeting(name: rx.Var[str]) -> rx.Component:
return rx.heading("Hello, " + name, as_="h2", size="5")
rx.vstack(
memo_greeting(name=MemoGreetState.name),
rx.input(value=MemoGreetState.name, on_change=MemoGreetState.change_name),
)Binding State to props at the call site doesn't drag the State into the page. The compiler moves that call into an auto-generated wrapper component, and the wrapper holds the State hooks it needs.
When the State changes, the wrapper re-renders, but if the prop values didn't actually change, React's memo stops there. The trick is to pass the expensive component only the Vars it really needs.
| Annotation | Values | Default | Use |
|---|---|---|---|
rx.Var[T] | str, int, TypedDict … | none | Regular prop. Treated as a Var inside the function |
rx.RestProp | at most one parameter | none | Passes through the remaining undeclared props (JSX's ...rest) |
children: rx.Var[rx.Component] | must be named children | none | Child components from the caller |
| defaults | rx.EMPTY_VAR_STR rx.EMPTY_VAR_INT rx.EMPTY_VAR_COMPONENT | none | Defaults must be Vars too: empty string, 0, empty component |
| call | memo_fn(title="...") | Props are passed by name (keyword) only | |
wrapper= | Var or None | React's memo | JavaScript function to wrap with |
To render a memo component for each item in a list, wrap the call in a lambda and pass the props as keywords. Pass a key that identifies each item too.
So that the key reaches the actual element, declare an rx.RestProp parameter and spread it into the returned component. You must not declare key directly in the signature.
The task inside the function is a Var, not a dict, so you can read from it like task["name"], but you can't iterate over it or call dict methods such as .keys().
Write docs
Review PR
from typing import TypedDict
class MemoTask(TypedDict):
id: str
name: str
class MemoTaskState(rx.State):
tasks: list[MemoTask] = [
{"id": "1", "name": "Write docs"},
{"id": "2", "name": "Review PR"},
]
@rx.event
def add_task(self):
n = len(self.tasks) + 1
self.tasks = [*self.tasks, {"id": str(n), "name": f"Task {n}"}][-6:]
@rx.memo
def memo_task_card(rest: rx.RestProp, *, task: rx.Var[MemoTask]) -> rx.Component:
return rx.card(rx.text(task["name"]), rest)
rx.vstack(
rx.foreach(
MemoTaskState.tasks,
lambda task: memo_task_card(task=task, key=task["id"]),
),
rx.button("Add task", on_click=MemoTaskState.add_task, variant="soft"),
)rx.RestProp receives undeclared props and passes them through. When you build a thin wrapper that only adds styling to a base component, you don't have to redeclare every prop.
Treat rest as an opaque value and pass it as a positional argument to the component that uses it. on_click, id and title all ride on rest to reach the button.
@rx.memo
def memo_pill_button(rest: rx.RestProp, *, label: rx.Var[str]) -> rx.Component:
return rx.button(label, rest, radius="full", variant="surface")
rx.flex(
memo_pill_button(label="Save", on_click=rx.toast("Saved"), id="memo_save"),
memo_pill_button(label="Help", on_click=rx.toast("Help is on the way"), title="Opens help"),
spacing="3",
)You can merge another dict into rest with .merge, and take values out with rest.get(...) for Var operations. But the values aren't known at compile time, so you can't branch on them with a Python if.
Using the official docs' example as is in Reflex 0.10 goes wrong in two places. rest holds the JavaScript names used in the browser (className), so "class_name" doesn't retrieve anything,
and the result of rest.get(...) is an untyped Var, so applying + " ..." directly raises an error. So the example reads and merges "className", and converts it to a string Var with .to(str) before concatenating.
@rx.memo
def memo_merged_button(rest: rx.RestProp, *, label: rx.Var[str]) -> rx.Component:
class_name = rest.get("className", "").to(str) + " font-bold tracking-wide"
return rx.button(label, rest.merge({"className": class_name}))
rx.flex(
memo_merged_button(label="Default"),
memo_merged_button(label="Caller class", class_name="italic"),
spacing="3",
)rx.RestProp passes props along at run time, so it can't carry values that the target component's create() uses at build time.
Examples are is_external on rx.link, tag on rx.icon, and keywords taken by a create() you wrote yourself. Sent through rest, they just become plain attributes or CSS and have no effect.
Take such values as separate rx.Var[...] parameters and pass them to create() directly in the function body.
class MemoPrefixText(rx.el.Span):
@classmethod
def create(cls, *children, prefix: rx.Var[str] | str = "", **props) -> rx.Component:
return super().create(prefix, *children, **props)
# prefix is used by create(), so it is not passed through rest.
# take it as its own parameter and pass it to create() in the memo body.
@rx.memo
def memo_styled_text(rest: rx.RestProp, *, prefix: rx.Var[str]) -> rx.Component:
return MemoPrefixText.create("Foo", rest, prefix=prefix)
memo_styled_text(prefix="P: ", class_name="italic")A parameter named children and annotated rx.Var[rx.Component] receives the caller's children. Pass children positionally and the other props as keywords.
Props whose default is rx.EMPTY_VAR_STR, like note, can be omitted.
Body copy goes here.
note is optional
@rx.memo
def memo_panel(
children: rx.Var[rx.Component],
*,
title: rx.Var[str],
note: rx.Var[str] = rx.EMPTY_VAR_STR,
) -> rx.Component:
return rx.box(
rx.heading(title, as_="h3", size="3"),
children,
rx.text(note, size="1", color_scheme="gray"),
padding="12px",
border=f"1px solid {rx.color('gray', 6)}",
border_radius="8px",
)
rx.flex(
memo_panel(rx.text("Body copy goes here."), title="Memoized card"),
memo_panel(rx.button("Inside", size="1"), title="With a note", note="note is optional"),
spacing="3",
wrap="wrap",
)Annotate the return as rx.Var[T] and you get a plain JavaScript function instead of a component, and the call site becomes a Var you can embed in the page.
The body of such a memo runs once at compile time, so only Var operations are allowed. No hooks, and no Python branching on Vars.
USD: $100
class MemoPriceState(rx.State):
amount: int = 100
currency: str = "USD"
@rx.event
def raise_price(self):
self.amount += 10
@rx.memo
def memo_format_price(amount: rx.Var[int], currency: rx.Var[str]) -> rx.Var[str]:
return currency.to(str) + ": $" + amount.to(str)
rx.hstack(
rx.text(memo_format_price(amount=MemoPriceState.amount, currency=MemoPriceState.currency), weight="bold"),
rx.button("+10", on_click=MemoPriceState.raise_price, size="1", variant="soft"),
align="center",
)The compiled function component is wrapped in React's memo by default. Give wrapper= another callable Var and it wraps with that instead; with wrapper=None, the function component is emitted unwrapped.
Below is an example of wrapper=None. It can only be used on memos that return a component; it is rejected on memos that return a Var.
@rx.memo(wrapper=None)
def memo_swatch(width: rx.Var[int], height: rx.Var[int]) -> rx.Component:
return rx.box(width=width.to_string() + "px", height=height.to_string() + "px", background=rx.color("indigo", 9), border_radius="4px")
rx.flex(
memo_swatch(width=40, height=20),
memo_swatch(width=80, height=20),
memo_swatch(width=120, height=20),
spacing="2",
align="center",
)To wrap with a function from another library, send the import along with a FunctionStringVar. The example below needs the mobx-react-lite package, so it is shown as code only.
observer = rx.vars.FunctionStringVar(
"observer",
_var_data=rx.vars.VarData(imports={"mobx-react-lite": "observer"}),
)
@rx.memo(wrapper=observer) # wrap with mobx's observer instead of React's memo
def observed_panel(label: rx.Var[str]) -> rx.Component:
return rx.text(label)Use it when rendering is expensive, the result is determined by a small number of props, and a frequently changing ancestor causes needless re-renders.
Skip it when the component is light enough that the bookkeeping costs more, or when the props change every time anyway, so memo never gets to skip a render.
The old rx.memo accepted plain type annotations such as title: str. Now rx.Var[...] is required. The old name rx._x.memo forwards to the new rx.memo and prints a notice once.
# before
@rx.memo
def card(title: str) -> rx.Component: ...
# now
@rx.memo
def card(title: rx.Var[str]) -> rx.Component: ...| Argument | Type | Use |
|---|---|---|
component_fn | Callable[..., rx.Component | rx.Var] | Function to memoize. Every parameter is rx.Var[...] or rx.RestProp |
wrapper | rx.Var | None | JS function to wrap with. Defaults to React's memo; None means no wrapping. Only for memos that return a component |
Use it directly, as in rx.memo(fn), or pass only keywords to make a decorator, as in rx.memo(wrapper=...)(fn).
The product card is split off with @rx.memo. The card only takes product, so even when Add one changes the cart count, the cards don't re-render.
width="100%" reaches the card via rest.
Desk lamp
$39Notebook
$6Fountain pen
$58from typing import TypedDict
class MemoProduct(TypedDict):
id: str
name: str
price: str
class MemoShopState(rx.State):
cart: int = 0
products: list[MemoProduct] = [
{"id": "p1", "name": "Desk lamp", "price": "$39"},
{"id": "p2", "name": "Notebook", "price": "$6"},
{"id": "p3", "name": "Fountain pen", "price": "$58"},
]
@rx.event
def add_one(self):
self.cart += 1
@rx.event
def empty_cart(self):
self.cart = 0
@rx.memo
def memo_product_card(rest: rx.RestProp, *, product: rx.Var[MemoProduct]) -> rx.Component:
return rx.card(
rx.flex(
rx.icon("package", size=18),
rx.text(product["name"], weight="bold"),
rx.spacer(),
rx.badge(product["price"], variant="soft"),
spacing="3",
align="center",
),
rest,
)
rx.vstack(
rx.hstack(
rx.badge(rx.icon("shopping-cart", size=14), MemoShopState.cart, size="2"),
rx.button("Add one", on_click=MemoShopState.add_one, size="1"),
rx.button("Empty", on_click=MemoShopState.empty_cart, size="1", variant="soft", color_scheme="gray"),
align="center",
),
rx.foreach(
MemoShopState.products,
lambda p: memo_product_card(product=p, key=p["id"], width="100%"),
),
width="100%",
max_width="380px",
)Workshop notes
@rx.memo makes a React component that re-renders only when its declared props change.
Every parameter is rx.Var[T] or rx.RestProp (at most one). Children are children: rx.Var[rx.Component].
Call it with keywords. Defaults are Vars such as rx.EMPTY_VAR_STR. In foreach, pass key through rest.
rest only carries run-time props. Values that create() uses at build time need their own parameters.
With -> rx.Var[T] it becomes a JS function. wrapper=None skips wrapping in memo.
Get news about new lessons
We'll email you when this course gets new lessons or a follow-up course opens.
R
ReflexKR
Learn Python and Reflex by doing. Every example was run and verified on Reflex 0.10.0.
ReflexKR is free, with no banner ads. If it helped you, you can support it with a coffee. ☕ Buy us a coffee
Affiliate disclosure© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0
Questions & answers
Ask anything about this lesson. Questions from every language version of this page appear together.