Rust Yew框架Hooks扩展库yew-hooks的使用,提供高效可复用的React式钩子功能

Rust Yew框架Hooks扩展库yew-hooks的使用,提供高效可复用的React式钩子功能

简介

Yew Hooks是一个为Yew框架设计的钩子扩展库,灵感来源于streamich/react-use、alibaba/hooks和vueuse/vueuse。

基本使用示例

use yew_hooks::prelude::*;

#[function_component(Counter)]
fn counter() -> Html {
    let counter = use_counter(0);  // 使用计数器钩子,初始值为0

    let onincrease = {
        let counter = counter.clone();
        Callback::from(move |_| counter.increase())  // 增加计数器
    };
    let ondecrease = {
        let counter = counter.clone();
        Callback::from(move |_| counter.decrease())  // 减少计数器
    };

    html! {
        <>
            <button onclick={onincrease}>{"Increase"}</button>
            <button onclick={ondecrease}>{"Decrease"}</button>
            <b>{"Current value: "}</b>
            { *counter }
        </>
    }
}

钩子分类

状态管理

  • use_toggle - 跟踪状态变化
  • use_bool_toggle - 跟踪布尔状态
  • use_counter - 跟踪数字状态
  • use_list - 跟踪列表状态
  • use_map - 跟踪哈希映射状态
  • use_set - 跟踪哈希集合状态
  • use_queue - 跟踪队列状态

副作用

  • use_async - 处理异步操作,如REST API调用
  • use_websocket - WebSocket通信
  • use_title - 设置页面标题
  • use_favicon - 设置页面favicon
  • use_local_storage - 管理localStorage中的值

生命周期

  • use_effect_once - 只运行一次的效果钩子
  • use_mount - 组件挂载时的回调
  • use_unmount - 组件卸载时的回调
  • use_is_mounted - 检查组件是否挂载

动画

  • use_timeout - 定时执行回调
  • use_interval - 间隔执行回调
  • use_raf - 在每次requestAnimationFrame时重新渲染组件

完整示例

计数器示例

use yew::prelude::*;
use yew_hooks::prelude::*;

#[function_component(Counter)]
fn counter() -> Html {
    let counter = use_counter(0);

    let onincrease = {
        let counter = counter.clone();
        Callback::from(move |_| counter.increase())
    };
    let ondecrease = {
        let counter = counter.clone();
        Callback::from(move |_| counter.decrease())
    };
    let onincreaseby = {
        let counter = counter.clone();
        Callback::from(move |_| counter.increase_by(10))
    };
    let ondecreaseby = {
        let counter = counter.clone();
        Callback::from(move |_| counter.decrease_by(10))
    };
    let onset = {
        let counter = counter.clone();
        Callback::from(move |_| counter.set(100))
    };
    let onreset = {
        let counter = counter.clone();
        Callback::from(move |_| counter.reset())
    };

    html! {
        <div>
            <button onclick={onincrease}>{"Increase"}</button>
            <button onclick={ondecrease}>{"Decrease"}</button>
            <button onclick={onincreaseby}>{"Increase by 10"}</button>
            <button onclick={ondecreaseby}>{"Decrease by 10"}</button>
            <button onclick={onset}>{"Set to 100"}</button>
            <button onclick={onreset}>{"Reset"}</button>
            <p>
                <b>{"Current value: "}</b>
                { *counter }
            </p>
        </div>
    }
}

异步请求示例

use serde::{de::DeserializeOwned, Deserialize, Serialize};
use yew::prelude::*;
use yew_hooks::prelude::*;

#[function_component(UseAsync)]
pub fn async_demo() -> Html {
    let state = use_async(async move { fetch_repo("jetli/yew-hooks".to_string()).await });

    let onclick = {
        let state = state.clone();
        Callback::from(move |_| {
            state.run();
        })
    };

    html! {
        <div>
            <button {onclick} disabled={state.loading}>{"Start to load repo: jetli/yew-hooks"}</button>
            <p>
                {
                    if state.loading {
                        html! { "Loading, wait a sec..." }
                    } else {
                        html! {}
                    }
                }
            </p>
            {
                if let Some(repo) = &state.data {
                    html! {
                        <>
                            <p>{"Repo name: "}<b>{ &repo.name }</b></p>
                            <p>{"Repo full name: "}<b>{ &repo.full_name }</b></p>
                            <p>{"Repo description: "}<b>{ &repo.description }</b></p>

                            <p>{"Owner name: "}<b>{ &repo.owner.login }</b></p>
                            <p>{"Owner avatar: "}<b><br/><img alt="avatar" src={repo.owner.avatar_url.clone()} /></b></p>
                        </>
                        }
                } else {
                    html! {}
                }
            }
            <p>
                {
                    if let Some(error) = &state.error {
                        match error {
                            Error::DeserializeError => html! { "DeserializeError" },
                            Error::RequestError => html! { "RequestError" },
                        }
                    else {
                        html! {}
                    }
                }
            </p>
        </div>
    }
}

async fn fetch_repo(repo: String) -> Result<Repo, Error> {
    fetch::<Repo>(format!("https://api.github.com/repos/{}", repo)).await
}

async fn fetch<T>(url: String) -> Result<T, Error>
where
    T: DeserializeOwned,
{
    let response = reqwest::get(url).await;
    if let Ok(data) = response {
        if let Ok(repo) = data.json::<T>().await {
            Ok(repo)
        } else {
            Err(Error::DeserializeError)
        }
    } else {
        Err(Error::RequestError)
    }
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
struct User {
    id: i32,
    login: String,
    avatar_url: String,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
struct Repo {
    id: i32,
    name: String,
    full_name: String,
    description: String,
    owner: User,
}

#[derive(Clone, Debug, PartialEq)]
enum Error {
    RequestError,
    DeserializeError,
}

WebSocket示例

use yew::prelude::*;
use yew_hooks::prelude::*;

#[function_component(UseWebSocket)]
pub fn web_socket() -> Html {
    let history = use_list(vec![]);
    let ws = use_websocket("wss://echo.websocket.events/".to_string());

    let onclick = {
        let ws = ws.clone();
        let history = history.clone();
        Callback::from(move |_| {
            let message = "Hello, world!".to_string();
            ws.send(message.clone());
            history.push(format!("[send]: {}", message));
        })
    };

    {
        let history = history.clone();
        let ws = ws.clone();
        use_effect_with(
            ws.message,
            move |message| {
                if let Some(message) = &**message {
                    history.push(format!("[recv]: {}", message.clone()));
                }
                || ()
            },
        );
    }

    html! {
        <div>
            <p>
                <button {onclick} disabled={*ws.ready_state != UseWebSocketReadyState::Open}>{"Send"}</button>
            </p>
            <p>
                <b>{"Message history: "}</b>
            </p>
            {
                for history.current().iter().map(|message| {
                    html! {
                        <p>{ message }</p>
                    }
                })
            }
        </div>
    }
}

安装

在项目目录中运行以下Cargo命令:

cargo add yew-hooks

或者在Cargo.toml中添加:

yew-hooks = "0.3.3"

许可证

Apache-2.0 或 MIT


1 回复

Rust Yew框架Hooks扩展库yew-hooks使用指南

概述

yew-hooks是一个为Yew框架提供的Hooks扩展库,它借鉴了React Hooks的概念,为Rust的Yew框架带来了高效、可复用的状态逻辑管理能力。这个库可以帮助开发者更简洁地组织组件逻辑,避免复杂的生命周期管理。

主要特点

  • 提供类似React Hooks的开发体验
  • 简化状态管理和副作用处理
  • 提高代码复用性
  • 与Yew框架无缝集成

安装方法

在Cargo.toml中添加依赖:

[dependencies]
yew = "0.19"
yew-hooks = "0.2"

常用Hooks及使用示例

1. use_state

用于在函数式组件中管理状态:

use yew::prelude::*;
use yew_hooks::use_state;

#[function_component(UseStateExample)]
fn state_example() -> Html {
    let counter = use_state(|| 0);
    
    let increment = {
        let counter = counter.clone();
        Callback::from(move |_| counter.set(*counter + 1))
    };
    
    html! {
        <div>
            <p>{ *counter }</p>
            <button onclick={increment}>{ "Increment" }</button>
        </div>
    }
}

2. use_effect

处理副作用,类似于React的useEffect:

use yew::prelude::*;
use yew_hooks::use_effect;

#[function_component(UseEffectExample)]
fn effect_example() -> Html {
    use_effect(|| {
        // 组件挂载时执行
        log::info!("Component mounted");
        
        // 清理函数,组件卸载时执行
        || log::info!("Component unmounted")
    });
    
    html! { <div>{ "Check your console logs" }</div> }
}

3. use_async

处理异步操作:

use yew::prelude::*;
use yew_hooks::use_async;
use reqwest;

#[function_component(UseAsyncExample)]
fn async_example() -> Html {
    let state = use_async(async {
        reqwest::get("https://api.example.com/data")
            .await?
            .json::<serde_json::Value>()
            .await
    });
    
    match &*state {
        Some(Ok(data)) => html! { <div>{ format!("Data: {:?}", data) }</div> },
        Some(Err(err)) => html! { <div>{ format!("Error: {}", err) }</div> },
        None => html! { <div>{ "Loading..." }</div> },
    }
}

4. use_interval

定时器Hook:

use yew::prelude::*;
use yew_hooks::use_interval;

#[function_component(UseIntervalExample)]
fn interval_example() -> Html {
    let counter = use_state(|| 0);
    
    {
        let counter = counter.clone();
        use_interval(
            move || {
                counter.set(*counter + 1);
            },
            1000, // 每1000ms执行一次
        );
    }
    
    html! { <div>{ *counter }</div> }
}

5. use_debounce

防抖Hook:

use yew::prelude::*;
use yew_hooks::use_debounce;

#[function_component(UseDebounceExample)]
fn debounce_example() -> Html {
    let query = use_state(String::new);
    let results = use_state(Vec::new);
    
    let on_input = {
        let query = query.clone();
        Callback::from(move |e: InputEvent| {
            let input: HtmlInputElement = e.target_unchecked_into();
            query.set(input.value());
        })
    };
    
    {
        let query = query.clone();
        let results = results.clone();
        use_debounce(
            move || {
                if !query.is_empty() {
                    // 模拟API调用
                    let new_results = vec![
                        format!("Result 1 for {}", query),
                        format!("Result 2 for {}", query),
                    ];
                    results.set(new_results);
                }
            },
            500, // 防抖延迟500ms
        );
    }
    
    html! {
        <div>
            <input type="text" oninput={on_input} />
            <ul>
                { for results.iter().map(|result| html! { <li>{ result }</li> }) }
            </ul>
        </div>
    }
}

自定义Hooks

yew-hooks也支持创建自定义Hooks,提高逻辑复用:

use yew::prelude::*;
use yew_hooks::{use_state, use_effect_with_deps};

pub fn use_window_width() -> i32 {
    let width = use_state(|| 0);
    
    use_effect_with_deps(
        move |_| {
            let window = web_sys::window().unwrap();
            let update_width = {
                let width = width.clone();
                move || width.set(window.inner_width().unwrap().as_f64().unwrap() as i32)
            };
            
            update_width();
            window.set_onresize(Some(update_width.as_ref().unchecked_ref()));
            
            move || {
                window.set_onresize(None);
            }
        },
        (),
    );
    
    *width
}

#[function_component(WindowWidthDisplay)]
fn window_width_display() -> Html {
    let width = use_window_width();
    
    html! { <div>{ format!("Window width: {}px", width) }</div> }
}

完整示例:待办事项应用

下面是一个使用yew-hooks构建的完整待办事项应用示例:

use yew::prelude::*;
use yew_hooks::{use_state, use_effect};

// 待办事项结构体
#[derive(Clone, Debug)]
struct Todo {
    id: usize,
    title: String,
    completed: bool,
}

#[function_component(TodoApp)]
fn todo_app() -> Html {
    // 使用use_state管理待办事项列表
    let todos = use_state(|| Vec::<Todo>::new());
    // 使用use_state管理输入框的值
    let input_value = use_state(|| String::new());

    // 添加新待办事项的回调
    let add_todo = {
        let todos = todos.clone();
        let input_value = input_value.clone();
        Callback::from(move |_| {
            let mut new_todos = (*todos).clone();
            new_todos.push(Todo {
                id: new_todos.len(),
                title: (*input_value).clone(),
                completed: false,
            });
            todos.set(new_todos);
            input_value.set(String::new());
        })
    };

    // 切换待办事项完成状态的回调
    let toggle_todo = {
        let todos = todos.clone();
        Callback::from(move |id: usize| {
            let mut new_todos = (*todos).clone();
            if let Some(todo) = new_todos.iter_mut().find(|t| t.id == id) {
                todo.completed = !todo.completed;
            }
            todos.set(new_todos);
        })
    };

    // 删除待办事项的回调
    let remove_todo = {
        let todos = todos.clone();
        Callback::from(move |id: usize| {
            let new_todos = (*todos)
                .iter()
                .filter(|t| t.id != id)
                .cloned()
                .collect::<Vec<_>>();
            todos.set(new_todos);
        })
    };

    // 输入框变化处理
    let on_input = {
        let input_value = input_value.clone();
        Callback::from(move |e: InputEvent| {
            let input: HtmlInputElement = e.target_unchecked_into();
            input_value.set(input.value());
        })
    };

    html! {
        <div class="todo-app">
            <h1>{ "待办事项" }</h1>
            <div class="add-todo">
                <input 
                    type="text" 
                    value={(*input_value).clone()} 
                    oninput={on_input} 
                    placeholder="添加新任务..."
                />
                <button onclick={add_todo} disabled={input_value.is_empty()}>
                    { "添加" }
                </button>
            </div>
            <ul class="todo-list">
                { for todos.iter().map(|todo| {
                    let todo_id = todo.id;
                    html! {
                        <li key={todo.id} class={if todo.completed { "completed" } else { "" }}>
                            <input 
                                type="checkbox" 
                                checked={todo.completed} 
                                onclick={toggle_todo.reform(move |_| todo_id)} 
                            />
                            <span>{ &todo.title }</span>
                            <button onclick={remove_todo.reform(move |_| todo_id)}>
                                { "删除" }
                            </button>
                        </li>
                    }
                }) }
            </ul>
        </div>
    }
}

// 应用入口
#[function_component(App)]
fn app() -> Html {
    html! {
        <div class="app">
            <TodoApp />
        </div>
    }
}

fn main() {
    yew::Renderer::<App>::new().render();
}

最佳实践

  1. 命名约定:自定义Hook应以"use_"开头,遵循React Hooks的命名约定
  2. 条件限制:不要在循环、条件或嵌套函数中调用Hooks
  3. 性能优化:对于耗时的计算,考虑使用use_memo来缓存结果
  4. 依赖数组:为use_effect等Hook提供正确的依赖数组,避免不必要的执行

yew-hooks为Yew开发者提供了更现代化、更简洁的状态管理方式,特别适合构建复杂的交互式Web应用。通过合理使用这些Hooks,可以显著提高代码的可维护性和复用性。

回到顶部