第四章:组件通信——Props、回调与 Context

博客v1.0系列教程(Dioxus)博客 v1.0 系列教程 (Dioxus)

第四章:组件通信——Props、回调与 Context

1. 通信方式总览

| 方式 | 数据流向 | 适用场景 | |------|---------|---------| | Props | 父 → 子 | 直接传值 | | EventHandler | 子 → 父 | 子组件通知事件 | | children 插槽 | 父 → 子 | 传递子元素 | | Context | 跨层级 | 全局/共享状态 |

2. Props 传值

2.1 基础传值

#[component]
fn UserAvatar(name: String, size: u32, avatar_url: Option<String>) -> Element {
    let size_class = format!("w-{} h-{}", size, size);

    rsx! {
        div { class: "flex items-center gap-2",
            if let Some(url) = avatar_url {
                img { class: "{size_class} rounded-full", src: "{url}" }
            } else {
                div { class: "{size_class} rounded-full bg-blue-500 flex items-center justify-center text-white",
                    "{name.chars().next().unwrap_or('?')}"
                }
            }
            span { "{name}" }
        }
    }
}

// 使用
UserAvatar {
    name: "张三".to_string(),
    size: 10,
    avatar_url: Some("/avatars/1.png".to_string()),
}

2.2 可选 Props 与默认值

#[component]
fn Badge(
    text: String,
    variant: Option<String>,     // 可选:颜色变体
    size: Option<String>,        // 可选:尺寸
    removable: Option<bool>,     // 可选:是否可移除
) -> Element {
    let variant = variant.unwrap_or_else(|| "default".to_string());
    let size = size.unwrap_or_else(|| "md".to_string());
    let removable = removable.unwrap_or(false);

    let variant_class = match variant.as_str() {
        "primary" => "bg-blue-100 text-blue-700",
        "success" => "bg-green-100 text-green-700",
        "danger" => "bg-red-100 text-red-700",
        _ => "bg-gray-100 text-gray-700",
    };

    let size_class = match size.as_str() {
        "sm" => "text-xs px-2 py-0.5",
        "lg" => "text-sm px-3 py-1",
        _ => "text-xs px-2.5 py-0.5",
    };

    rsx! {
        span { class: "inline-flex items-center gap-1 rounded-full {variant_class} {size_class}",
            "{text}"
            if removable {
                button { class: "hover:opacity-70", "×" }
            }
        }
    }
}

3. EventHandler —— 子传父

3.1 基础回调

#[component]
fn InputField(
    label: String,
    on_change: EventHandler<String>,   // 事件回调
) -> Element {
    rsx! {
        div { class: "mb-4",
            label { class: "block text-sm mb-1", "{label}" }
            input {
                class: "border rounded px-3 py-2 w-full",
                oninput: move |e| {
                    // 将 input 的值通过回调传递给父组件
                    on_change.call(e.value());
                },
            }
        }
    }
}

// 父组件
#[component]
fn RegisterForm() -> Element {
    let username = use_signal(String::new);
    let email = use_signal(String::new);

    rsx! {
        form {
            InputField {
                label: "用户名".to_string(),
                on_change: move |val| username.set(val),
            }
            InputField {
                label: "邮箱".to_string(),
                on_change: move |val| email.set(val),
            }
        }
    }
}

3.2 多参数回调

#[component]
fn Pagination(
    on_page_change: EventHandler<(usize, usize)>,  // (当前页, 总页数)
) -> Element {
    rsx! {
        button {
            onclick: move |_| on_page_change.call((1, 10)),
            "跳转到第1页"
        }
    }
}

3.3 无参数事件

#[component]
fn DeleteButton(on_confirm: EventHandler<()>) -> Element {
    rsx! {
        button {
            class: "text-red-500",
            onclick: move |_| on_confirm.call(()),
            "删除"
        }
    }
}

// 使用
DeleteButton {
    on_confirm: move |_| println!("确认删除"),
}

4. Children 插槽

#[component]
fn Panel(title: String, children: Element) -> Element {
    rsx! {
        div { class: "border rounded-lg overflow-hidden",
            style: "background: var(--card); border-color: var(--border);",
            div { class: "px-4 py-3 font-semibold border-b",
                style: "background: var(--hover); border-color: var(--border);",
                "{title}"
            }
            div { class: "p-4",
                {children}
            }
        }
    }
}

// 使用:将子元素传入插槽
Panel { title: "文章列表".to_string(),
    ul {
        li { "文章 1" }
        li { "文章 2" }
        li { "文章 3" }
    }
}

5. Context 跨层级共享

当数据需要跨越多个组件层级时,逐层传递 Props 会变得冗余。Context 解决了这个问题。

5.1 提供 Context

#[component]
fn App() -> Element {
    // 在顶层提供主题配置
    let theme = use_signal(|| "light".to_string());
    use_context_provider(|| theme);

    rsx! {
        NavBar {}
        Content {}
    }
}

5.2 消费 Context

#[component]
fn ThemeToggle() -> Element {
    // 任意深度的子组件都可以直接读取
    let mut theme = use_context::<Signal<String>>();

    rsx! {
        button {
            onclick: move |_| {
                let next = if theme() == "light" { "dark" } else { "light" };
                theme.set(next.to_string());
            },
            "切换到{theme}"
        }
    }
}

5.3 多个 Context

// 可以同时提供多个 Context
#[component]
fn Providers(children: Element) -> Element {
    let user = use_signal(|| Option::<User>::None);
    let theme = use_signal(|| "light".to_string());
    let lang = use_signal(|| "zh-CN".to_string());

    use_context_provider(|| user);
    use_context_provider(|| theme);
    use_context_provider(|| lang);

    rsx! { {children} }
}

6. 通信方式选择指南

需要传递数据?
├── 只在父子之间
│   ├── 父→子 → Props
│   └── 子→父 → EventHandler
├── 兄弟组件
│   ├── 简单 → 提升状态到共同父级
│   └── 复杂 → Context
├── 跨多层组件 → Context
└── 子元素插入 → children 插槽

7. 小结

  • Props 是父传子的基本方式,支持必填和可选参数
  • EventHandler 实现子传父的事件通知
  • children 插槽让父组件控制子元素的渲染
  • use_context_provider + use_context 实现跨层级共享
  • 选择合适的通信方式保持组件树的简洁
  • 下一章将学习列表渲染与条件渲染
dioxuspropsevent-handlercontextchildren