组件工坊:Reflex 组件库 100 例
·
课程概览
布局
·
Box
·
Flex
·
Stack
·
Grid
·
Container
·
Section
·
Center
·
Spacer
·
Separator
·
Card
·
Inset
·
Aspect Ratio
·
Fragment
排版
·
Text
·
Heading
·
Link
·
Code
·
Markdown
·
Blockquote
·
Quote
·
Strong
·
Em
·
Kbd
表单
·
Button
·
Input
·
Text Area
·
Checkbox
·
Switch
·
Radio Group
·
Select
·
Slider
·
Form
·
Upload
数据展示
·
Badge
·
Avatar
·
Icon
·
Callout
·
List
·
Data List
·
Code Block
·
Progress
·
Spinner
·
Scroll Area
·
Moment
展开与切换
·
Tabs
·
Accordion
·
Segmented Control
浮层
·
Tooltip
·
Popover
·
Hover Card
·
Dialog
·
Alert Dialog
·
Drawer
·
Dropdown Menu
·
Context Menu
·
Toast
动态渲染
·
Cond
·
Match
·
Foreach
·
Auto Scroll
媒体
·
Image
·
Audio
·
Video
表格与数据网格
·
Table
·
Data Table
·
Data Editor
图表
·
Area Chart
·
Bar Chart
·
Line Chart
·
Composed Chart
·
Pie Chart
·
Radar Chart
·
Radial Bar Chart
·
Scatter Chart
·
Funnel Chart
·
Treemap
·
Sankey Chart
·
Error Bar
图表部件
·
Axis
·
Cartesian Grid
·
Chart Tooltip
·
Legend
·
Label
·
Reference
·
Brush
其他图表库
·
Plotly
·
Pyplot
HTML 元素
·
HTML
·
HTML Text
·
HTML Layout
·
HTML Forms
·
HTML Media
·
HTML Tables
·
SVG
其他工具
·
Theme
·
Skeleton
·
Clipboard
·
Html Embed
·
Script
·
Memo
工坊订单
请把商品卡片单独拆出来,让购物车数量每次变化时,旁边的商品卡片列表不必跟着重绘。
@rx.memo 把函数变成经过记忆化(memoize)的 React 组件。编译器把这个函数导出为单独的模块,React 的 memo 只在声明过的属性变化时才重绘它。
适用于渲染开销大、只依赖状态中一小部分的子树。本页的 @rx.memo 函数都定义在示例代码的上方(State 源码部分)。
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 只在 label 变化时重绘。用 Increment 增加 count,这个组件也不会重绘。
rx.memo 没有可见的属性,所以这里不用实验台,而是用 State 驱动的示例来演示。属性可以直接传入 State 的 Var,这个 Var 一变就会重绘。
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),
)即使在调用处把 State 绑定到属性上,State 也不会被拉进页面。编译器会把这个调用移到自动生成的包装组件里,由包装组件持有所需的 State hook。
State 变化时包装组件会重绘,但如果属性值实际上没变,React 的 memo 就会在那里停下。诀窍是只给开销大的组件传它真正需要的 Var。
| 标注 | 取值 | 默认值 | 用途 |
|---|---|---|---|
rx.Var[T] | str, int, TypedDict … | 无 | 普通属性。在函数里作为 Var 处理 |
rx.RestProp | 最多一个参数 | 无 | 把未声明的其余属性原样传下去(相当于 JSX 的 ...rest) |
children: rx.Var[rx.Component] | 名字必须是 children | 无 | 调用方的子组件 |
| 默认值 | rx.EMPTY_VAR_STR rx.EMPTY_VAR_INT rx.EMPTY_VAR_COMPONENT | 无 | 默认值也必须是 Var:空字符串·0·空组件 |
| 调用 | memo_fn(title="...") | 属性只能按名字(关键字)传入 | |
wrapper= | Var 或 None | React 的 memo | 用来包裹的 JavaScript 函数 |
想为列表的每一项渲染一个 memo 组件,就把调用包进 lambda,并用关键字传属性。区分各项的 key 也要一起传。
为了让 key 落到真正的元素上,要声明一个 rx.RestProp 参数,并把它展开到返回的组件里。不要在函数签名里直接声明 key。
函数里的 task 不是 dict 而是 Var,可以像 task["name"] 这样取值,但不能遍历,也不能调用 .keys() 这类 dict 方法。
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 接收未声明的属性并原样传下去。给基础组件套一层只改外观的薄包装时,不用把属性一个个重新声明。
把 rest 看作一个无法窥视内部的值,作为位置参数传给要用的组件。on_click、id、title 都会通过 rest 到达按钮。
@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",
)可以用 .merge 把别的 dict 合并进 rest,也可以用 rest.get(...) 取出值做 Var 运算。不过这些值在编译时无法得知,所以不能用 Python 的 if 来分支。
在 Reflex 0.10 中原样使用官方文档的示例,会有两处对不上。rest 里存的是浏览器使用的 JavaScript 名字(className),用 "class_name" 取不出来;
而 rest.get(...) 的结果是类型未定的 Var,直接 + " ..." 会报错。所以要用 "className" 取出和合并,并先用 .to(str) 转成字符串 Var 再相加。
@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 是在运行时传递属性的,所以无法携带目标组件的 create() 在构建时使用的值。
rx.link 的 is_external、rx.icon 的 tag、自己写的 create() 接收的关键字参数都属于这一类。用 rest 传过去只会变成普通属性或 CSS,没有任何效果。
这类值要另外用 rx.Var[...] 参数接收,在函数体里直接传给 create()。
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")名字为 children、标注为 rx.Var[rx.Component] 的参数接收调用方的子元素。子元素用位置参数传,其余属性用关键字传。
像 note 这样默认值是 rx.EMPTY_VAR_STR 的属性可以省略。
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",
)把返回标注写成 rx.Var[T],生成的就不是组件,而是一个普通的 JavaScript 函数,调用处会成为可以嵌进页面的 Var。
这种 memo 的函数体只在编译时执行一次,所以只能用 Var 运算。不能用 hook,也不能根据 Var 做 Python 分支。
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",
)编译出的函数组件默认用 React 的 memo 包裹。给 wrapper= 传入另一个可调用的 Var,就用它来包裹;wrapper=None 则直接导出不加包裹的函数组件。
下面是 wrapper=None 的例子。它只能用于返回组件的 memo,用在返回 Var 的 memo 上会被拒绝。
@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",
)用其他库的函数包裹时,要在 FunctionStringVar 里带上 import。下面的例子需要 mobx-react-lite 包,所以只以代码展示。
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)用:渲染开销大、结果由少数几个属性决定、又因为频繁变化的祖先组件而被白白重绘时。
不用:组件很轻,记账的开销反而更大时;或者属性反正每次都会变,memo 一次也跳不过去时。
以前的 rx.memo 接受 title: str 这样的普通类型标注,现在需要 rx.Var[...]。旧名字 rx._x.memo 会转到新的 rx.memo,并打印一次提示信息。
# before
@rx.memo
def card(title: str) -> rx.Component: ...
# now
@rx.memo
def card(title: rx.Var[str]) -> rx.Component: ...| 参数 | 类型 | 用途 |
|---|---|---|
component_fn | Callable[..., rx.Component | rx.Var] | 要做成 memo 的函数。参数必须全是 rx.Var[...] 或 rx.RestProp |
wrapper | rx.Var | None | 用来包裹的 JS 函数。默认是 React 的 memo,None 则不包裹。仅用于返回组件的 memo |
可以像 rx.memo(fn) 这样直接用,也可以像 rx.memo(wrapper=...)(fn) 这样只给关键字参数来生成装饰器。
用 @rx.memo 把商品卡片拆了出来。卡片只接收 product,所以用 Add one 改变购物车数量时,卡片不会重绘。
width="100%" 通过 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",
)工坊笔记
@rx.memo 生成只在声明的属性变化时才重绘的 React 组件。
参数必须全是 rx.Var[T] 或 rx.RestProp(最多一个)。子元素用 children: rx.Var[rx.Component]。
调用时用关键字。默认值是 rx.EMPTY_VAR_STR 这类 Var。在 foreach 中 key 走 rest。
rest 只传运行时的属性。create() 在构建时使用的值要另设参数。
写成 -> rx.Var[T] 就会变成 JS 函数。wrapper=None 表示不用 memo 包裹。
订阅新课时和新课程通知
本课程新增课时或推出后续课程时,我们会发邮件通知你。
问答
关于本课有任何疑问都可以在这里留言。所有语言版本页面的提问会汇集在一起。