第十一章:组件通信进阶——Context、回调与跨层级数据流

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

第十一章:组件通信进阶——Context、回调与跨层级数据流

1. 组件通信的几种模式

在 Dioxus 中,组件之间的通信主要有以下几种方式:

| 模式 | 适用场景 | 数据流向 | |------|---------|---------| | Props 传递 | 父→子 直接传值 | 单向向下 | | 回调函数 | 子→父 通知事件 | 单向向上 | | Context | 跨层级共享 | 任意层级 | | Signal 提升 | 兄弟组件共享 | 提升到共同父级 |

2. Props 深度用法

2.1 可选 Props 与默认值

#[component]
fn Card(
    title: String,
    subtitle: Option<String>,      // 可选
    variant: Option<String>,       // 可选
    children: Element,             // 子元素插槽
) -> Element {
    let sub = subtitle.unwrap_or_default();
    let variant_cls = match variant.as_deref() {
        Some("primary") => "border-primary",
        Some("danger") => "border-red-500",
        _ => "border-gray-200",
    };

    rsx! {
        div { class: "rounded-lg border p-4 {variant_cls}",
            h3 { class: "font-bold", "{title}" }
            if !sub.is_empty() {
                p { class: "text-sm text-gray-500", "{sub}" }
            }
            {children}
        }
    }
}

// 使用
Card {
    title: "Hello".to_string(),
    variant: Some("primary".to_string()),
    p { "这是卡片内容" }
}

2.2 组件 Props 与泛型

当需要传递不同类型的数据时,可以使用泛型 Props:

#[derive(Props, Clone, PartialEq)]
struct ListProps<T: 'static + Clone + PartialEq> {
    items: Vec<T>,
    render_item: Box<dyn Fn(&T) -> Element>,
}

// 但 Dioxus 推荐使用更简单的方式 —— 用回调
#[component]
fn ItemList<T>(
    items: Vec<T>,
    render_item: EventHandler<(T,)>,
) -> Element where T: 'static + Clone + PartialEq {
    // 实践中直接用 EventHandler 更简洁
}

3. 回调函数与子传父

3.1 基础回调

#[component]
fn SearchInput(on_search: EventHandler<String>) -> Element {
    let mut value = use_signal(String::new);

    rsx! {
        input {
            value: "{value}",
            oninput: move |e| {
                let v = e.value();
                value.set(v.clone());
                on_search.call(v);
            }
        }
    }
}

// 父组件
#[component]
fn Parent() -> Element {
    let results = use_signal(|| Vec::<String>::new());

    rsx! {
        SearchInput {
            on_search: move |query| {
                // 当子组件搜索时,父组件更新状态
                let data = perform_search(&query);
                results.set(data);
            }
        }
    }
}

3.2 多参数回调

// 使用元组传递多个值
#[component]
fn Pagination(on_page_change: EventHandler<(usize, usize)>) -> Element {
    // ...
    on_page_change.call((current_page, total_pages));
}

4. Context 跨层级共享

这是最强大的通信方式,适合主题、用户信息、配置等需要跨多层组件的数据。

4.1 提供 Context

#[component]
fn App() -> Element {
    // 创建一个可共享的信号
    let user = use_signal(|| Option::<User>::None);
    let theme = use_signal(|| "light".to_string());

    // 通过 Context Provider 注入到整个子树
    use_context_provider(|| user);
    use_context_provider(|| theme);

    rsx! {
        Router::<Route> {}
    }
}

4.2 消费 Context

任意深度的子组件都可以直接读取:

#[component]
fn NavBar() -> Element {
    // 从 Context 读取用户信息和主题
    let user = use_context::<Signal<Option<User>>>();
    let theme = use_context::<Signal<String>>();

    rsx! {
        header {
            style: "background: if theme() == \"dark\" { \"#1a1a2e\" } else { \"#fff\" }",
            if let Some(u) = &*user.read() {
                span { "欢迎, {u.username}" }
                button { onclick: move |_| user.set(None), "退出" }
            } else {
                a { href: "/login", "登录" }
            }
        }
    }
}

4.3 更新 Context

// 登录页面中更新用户 Context
#[component]
fn LoginPage() -> Element {
    let mut user = use_context::<Signal<Option<User>>>();

    let do_login = move |username, password| {
        // ... 登录逻辑
        user.set(Some(User {
            id: 1,
            username: username.clone(),
            avatar: None,
        }));
    };
    // ...
}

5. Signal 提升(Lifting State Up)

当两个兄弟组件需要共享数据时,将状态提升到它们的共同父级:

#[component]
fn BlogPage() -> Element {
    // 状态提升到这里
    let selected_category = use_signal(|| "all".to_string());
    let search_query = use_signal(String::new);

    rsx! {
        div { class: "flex gap-4",
            // 左侧筛选栏 —— 可以修改 selected_category
            SidebarFilter {
                selected: selected_category,
                on_select: move |cat| selected_category.set(cat),
            }
            // 右侧文章列表 —— 读取 selected_category 和 search_query
            ArticleList {
                category: selected_category(),
                query: search_query(),
            }
        }
    }
}

6. 实战:全局通知系统

使用 Context 实现一个全局的消息通知组件:

// 通知类型
#[derive(Clone)]
struct Notification {
    id: u64,
    message: String,
    level: String, // "info", "success", "error"
}

// 通知管理器
#[component]
fn NotificationProvider(children: Element) -> Element {
    let notifications = use_signal(|| Vec::<Notification>::new());
    use_context_provider(|| notifications);

    rsx! {
        div { class: "relative",
            // 通知浮层
            div { class: "fixed top-4 right-4 z-50 flex flex-col gap-2",
                for notif in notifications.iter() {
                    div {
                        class: "px-4 py-2 rounded-lg shadow-lg text-sm text-white transition-all",
                        style: "background: match notif.level.as_str() {
                            \"error\" => \"#ef4444\",
                            \"success\" => \"#22c55e\",
                            _ => \"#3b82f6\",
                        }",
                        "{notif.message}"
                    }
                }
            }
            {children}
        }
    }
}

// 任意组件中发送通知
fn use_notify() -> impl Fn(String, String) {
    let notifications = use_context::<Signal<Vec<Notification>>>();
    move |msg: String, level: String| {
        let id = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap()
            .as_millis() as u64;
        notifications.write().push(Notification { id, message: msg, level });
    }
}

7. 选择通信方式的决策树

需要传递数据?
├── 父→子 直接传递 → Props
├── 子→父 通知事件 → EventHandler 回调
├── 兄弟组件共享 → Signal 提升到共同父级
├── 跨多层组件共享 → Context
└── 全局一次性数据(路由参数等)→ 路由状态

8. 小结

  • Props 是最基础最安全的通信方式,类型安全、显式
  • EventHandler 实现了子传父的事件通知模式
  • use_context/use_context_provider 适合跨层级共享状态
  • Signal 提升是兄弟组件共享状态的标准做法
  • 合理选择通信方式可以保持组件树的简洁和可维护性
dioxuscomponentscontextcallbackdata-flow