学习目标
本课解决两个问题:父组件如何向子组件传递数据,以及子组件如何向父组件回传消息。
知识点1:Props 基础
组件通过函数参数接收外部数据,#[component] 宏会自动把这些参数转为 Props 结构体。
usedioxus::prelude::*;// 定义一个接收属性的组件#[component]fnGreeting(name:String,age:u32)->Element{rsx!{p{"你好,我叫 {name},今年 {age} 岁"}}}// 父组件调用时传入属性#[component]fnApp()->Element{rsx!{// 属性名和组件参数名一一对应Greeting{name:"张三".to_string(),age:25,}}}属性类型规则
- 属性可以是任何类型:String、u32、bool、Vec、自定义结构体等
- 传入的值必须和参数类型完全匹配
- 组件名必须大写开头,属性名必须小写开头
知识点2:可选属性与默认值
不是每个属性都必须传,用 Option 或 #[props(default)] 实现可选。
#[component]fnButton(label:String,#[props(default)]disabled:bool,// 默认 false#[props(default ="blue".to_string())]color:String,// 默认 "blue"#[props(into)]size:String,// into:自动调用 .into() 转换)->Element{rsx!{button{disabled:disabled,style:"color: {color};",class:"btn-{size}","{label}"}}}#[component]fnApp()->Element{rsx!{// 只传必填属性,其余用默认值Button{label:"提交".to_string()}// 覆盖部分默认值Button{label:"删除".to_string(),disabled:true,color:"red".to_string(),}// #[props(into)] 允许直接传 &str,自动转 StringButton{label:"取消".to_string(),size:"large",// 不需要 .to_string()}}}三种可选机制对比:
机制 写法 效果
Option icon: Option 不传则为 None
#[props(default)] #[props(default)] x: bool 不传则用类型的默认值(false、0、“” 等)
#[props(default = expr)] #[props(default = 42)] x: i32 不传则用指定表达式的值
#[props(into)] #[props(into)] name: String 传值时自动调用 .into() 转换
知识点3:子元素传递 — children
组件可以像 HTML 标签一样包裹子内容,用 children 参数接收。
// children 类型是 Element,代表传入的子节点#[component]fnCard(title:String,children:Element)->Element{rsx!{div{class:"card",div{class:"card-header","{title}"}div{class:"card-body",{children}}// 用 { } 插入子元素}}}#[component]fnApp()->Element{rsx!{// 像 HTML 标签一样包裹内容Card{title:"用户信息".to_string(),p{"姓名:张三"}p{"年龄:25"}}Card{title:"订单详情".to_string(),ul{li{"商品A × 2"}li{"商品B × 1"}}}}}知识点4:组件通信 — Signal 下传
父组件创建 Signal,通过 Props 传给子组件,子组件自动响应变化。
#[component]fnParent()->Element{// 父组件创建信号letcount=use_signal(||0);rsx!{// 把信号传给子组件Display{count:count}Controls{count:count}}}// 子组件接收 Signal 类型#[component]fnDisplay(count:Signal<i32>)->Element{rsx!{// 读取信号值,父组件修改时自动更新p{"当前值:{count}"}}}#[component]fnControls(count:Signal<i32>)->Element{rsx!{button{onclick:move|_|count+=1,"+1"}button{onclick:move|_|count-=1,"-1"}}}数据流向:
Parent(创建 Signal)
├── Display(读取 Signal)← 自动响应变化
└── Controls(修改 Signal)→ 触发 Display更新
知识点5:组件通信 — 回调函数上抛
子组件不直接修改父组件状态,而是通过回调函数通知父组件"发生了什么"。
#[component]fnParent()->Element{letmutitems=use_signal(||vec!["苹果".to_string(),"香蕉".to_string()]);rsx!{ul{foriteminitems.iter(){// 把删除回调传给子组件ListItem{key:"{item}",text:item.clone(),on_delete:move||{// 父组件决定如何处理删除items.retain(|x|x!=item.as_str());},}}}}}// 子组件接收回调函数#[component]fnListItem(text:String,on_delete:EventHandler<()>,// 回调类型)->Element{rsx!{li{span{"{text}"}button{onclick:move|_|on_delete.call(()),// 触发回调"删除"}}}}回调类型选择:
类型 说明 适用场景
EventHandler Dioxus 专用回调,用 .call(value) 触发 大多数场景,推荐
EventHandler<()> 不带数据的回调 只需通知"发生了",不传数据
EventHandler 带数据的回调 需要传递具体值(如索引、ID)
知识点6:带数据的回调
#[component]fnParent()->Element{letmutselected=use_signal(||String::new());rsx!{p{"选中:{selected}"}ColorPicker{on_select:move|color:String|{selected.set(color);},}}}#[component]fnColorPicker(on_select:EventHandler<String>)->Element{letcolors=vec!["红","绿","蓝"];rsx!{div{forcolorincolors{button{key:"{color}",onclick:move|_|on_select.call(color.to_string()),"{color}"}}}}}知识点7:双向绑定模式
结合 Signal 下传和回调上抛,实现父子组件的双向数据流。
#[component]fnParent()->Element{letmuttext=use_signal(||String::from("初始文本"));rsx!{p{"父组件看到的值:{text}"}// 方式1:直接传 Signal(子组件可直接读写)Editor{text:text}}}// 子组件直接持有 Signal,可读可写#[component]fnEditor(text:Signal<String>)->Element{rsx!{input{value:"{text}",oninput:move|event|text.set(event.value()),}}}核心规则
概念 说明
Props 组件函数参数,#[component] 自动转为结构体
#[props(default)] 属性可选,不传用类型默认值
#[props(default = expr)] 属性可选,不传用指定表达式
#[props(into)] 传值时自动 .into() 转换
children: Element 接收子元素,用 {children} 插入
Signal 下传 父组件创建 Signal,子组件接收 Signal 类型
EventHandler 子组件回调父组件,用 .call(value) 触发
双向绑定 子组件持有 Signal,直接读写父组件状态
动手试试
补全下面的代码:
usedioxus::prelude::*;fnmain(){dioxus::launch(App);}#[component]fnApp()->Element{rsx!{div{style:"padding: 20px; font-family: sans-serif;",// === 题目1:头像组件 ===// 调用 Avatar 组件,传入 name: "李四"、size: 48Avatar{// 补全属性}hr{}// === 题目2:折叠面板 ===Accordion{title:"什么是 Rust?".to_string(),// 补全:传入子内容 "Rust 是一门系统编程语言,注重安全性和性能。"}hr{}// === 题目3:评分组件 ===RatingDemo{}}}}// === 题目1:头像组件 ===// 要求:// - 接收 name: String(用户名)和 size: u32(头像尺寸,像素)// - 渲染一个圆形 div,宽高为 size 像素,背景色 #4A90D9// - 内部显示用户名第一个字(白色、居中、字号为 size/2)// - 提示:用 name.chars().next().unwrap_or('?') 取第一个字符#[component]fnAvatar(// 补全属性定义)->Element{// 补全渲染逻辑todo!()}// === 题目2:折叠面板 ===// 要求:// - 接收 title: String 和 children: Element// - 用 use_signal 存储 is_open,初始值 false// - 点击标题区域切换展开/收起状态// - 展开时显示 children 内容,收起时隐藏// - 标题旁显示 "▼"(展开)或 "▶"(收起)#[component]fnAccordion(// 补全属性定义)->Element{// 补全todo!()}// === 题目3:评分组件 ===// 要求:// - RatingDemo 组件用 use_signal 存储评分 rating,初始值 3// - 显示当前评分 "评分:3 / 5"// - 调用 StarRating 子组件,传入 rating 和 on_change 回调// - StarRating 组件:// - 接收 rating: u32 和 on_change: EventHandler<u32>// - 渲染 5 个星星按钮(★)// - 点击第 N 个星星,调用 on_change.call(N)// - 已选中的星星显示金色(color: gold),未选中显示灰色(color: gray)#[component]fnRatingDemo()->Element{// 补全todo!()}#[component]fnStarRating(// 补全属性定义)->Element{// 补全todo!()}期望效果
- 题目1:显示蓝色圆形头像,内有"李"字,尺寸 48px
- 题目2:初始收起显示"▶ 什么是 Rust?“,点击展开显示内容和"▼”,再点击收起
- 题目3:显示 5 个星星,前 3 个金色,点击星星可切换评分
写好了贴过来,没问题就进入第4课:事件处理与表单 👋
贴代码
Dioxus‑0.7 完整参考答案(头像、折叠面板、星级评分,含组件插槽 children、EventHandler回调)
Cargo.toml
[package] name = "dioxus-lesson" version = "0.1.0" edition = "2021" [dependencies] dioxus = "0.7"完整可运行源码
usedioxus::prelude::*;fnmain(){dioxus::launch(App);}#[component]fnApp()->Element{rsx!{div{style:"padding: 20px; font-family: sans-serif;",// === 题目1:头像组件 ===Avatar{name:"李四".to_string(),size:48,}hr{}// === 题目2:折叠面板 ===Accordion{title:"什么是 Rust?".to_string(),children:rsx!{p{"Rust 是一门系统编程语言,注重安全性和性能。"}}}hr{}// === 题目3:评分组件 ===RatingDemo{}}}}// === 题目1:头像组件 ===#[component]fnAvatar(name:String,size:u32)->Element{letfirst_char=name.chars().next().unwrap_or('?');letfont_size=size/2;rsx!{div{style:"width:{size}px;height:{size}px;border-radius:50%;background:#4A90D9;color:white;display:flex;align-items:center;justify-content:center;font-size:{font_size}px;","{first_char}"}}}// === 题目2:折叠面板(children插槽) ===#[component]fnAccordion(title:String,children:Element)->Element{letmutis_open=use_signal(||false);rsx!{div{style:"border:1px solid #ccc;border-radius:6px;overflow:hidden;",div{style:"padding:10px;cursor:pointer;background:#f5f5f5;",onclick:move|_|is_open.toggle(),span{ifis_open(){"▼"}else{"▶"}}span{style:"margin-left:6px;","{title}"}}ifis_open(){div{style:"padding:10px;",{children}}}}}}// === 题目3:评分组件(父子通信 EventHandler) ===#[component]fnRatingDemo()->Element{letmutrating=use_signal(||3u32);rsx!{div{style:"padding:10px;",p{"评分:{rating} / 5"}StarRating{rating:rating(),on_change:move|new_val|rating.set(new_val),}}}}#[component]fnStarRating(rating:u32,on_change:EventHandler<u32>)->Element{rsx!{div{foriin1..=5{button{key:"{i}",style:"border:none;background:none;font-size:28px;cursor:pointer;color:{if i <= rating {"gold"} else {"gray"}};",onclick:move|_|on_change.call(i),"★"}}}}}核心知识点讲解
1. 头像组件
- chars().next() 获取字符串第一个汉字/字符;
- flex 布局实现文字在圆形头像内水平垂直居中;
- 尺寸通过 props 传入,字号跟随头像大小动态计算。
2. Accordion 折叠面板(children插槽)
- children:Element 是Dioxus组件插槽,父组件可以往里面传入任意rsx内容;
- is_open.toggle() :信号一键取反,简化展开收起逻辑;
- 点击标题切换显示/隐藏子内容。
3. StarRating星级评分、父子组件回调通信 EventHandler
- 父组件 RatingDemo 保存状态 rating ;
- 子组件 StarRating 只负责UI渲染,不保存状态;
- 点击星星 → on_change.call(i) 回调通知父组件更新评分,实现单向数据流。
关键点:
子组件只读rating,不能直接修改;状态提升到父组件,回调更新,这是Dioxus组件通信标准写法。