2月19日 16:23

How to Handle Asynchronous Data in Yew and What Are the Common Async Processing Patterns?

Asynchronous Data Processing in Yew

Yew provides powerful asynchronous data processing capabilities, elegantly handling async operations through wasm-bindgen-futures and Rust's async/await syntax.

Basic Async Operations

1. Using send_future for Async Tasks

rust
use yew::prelude::*; use wasm_bindgen_futures::spawn_local; #[function_component(AsyncExample)] fn async_example() -> Html { let data = use_state(|| String::new()); let loading = use_state(|| false); let fetch_data = { let data = data.clone(); let loading = loading.clone(); Callback::from(move |_| { loading.set(true); spawn_local(async move { // Simulate async operation gloo_timers::future::sleep(std::time::Duration::from_secs(1)).await; data.set("Async data loaded!".to_string()); loading.set(false); }); }) }; html! { <div> <button onclick={fetch_data} disabled={*loading}> { if *loading { "Loading..." } else { "Fetch Data" } } </button> <p>{ &*data }</p> </div> } }

2. Using use_future Hook

rust
use yew::prelude::*; use yew_hooks::prelude::*; #[function_component(UseFutureExample)] fn use_future_example() -> Html { let state = use_future(|| async { // Simulate API call gloo_timers::future::sleep(std::time::Duration::from_secs(2)).await; "Data from future!".to_string() }); html! { <div> { match &*state { UseFutureState::Pending => html! { <p>{ "Loading..." }</p> }, UseFutureState::Ready(data) => html! { <p>{ data }</p> }, UseFutureState::Failed(_) => html! { <p>{ "Error loading data" }</p> }, }} </div> } }

HTTP Request Handling

1. Using reqwest for HTTP Requests

toml
[dependencies] reqwest = { version = "0.11", features = ["json"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0"
rust
use serde::{Deserialize, Serialize}; use yew::prelude::*; #[derive(Debug, Serialize, Deserialize, Clone)] pub struct User { pub id: u32, pub name: String, pub email: String, } #[function_component(UserFetcher)] fn user_fetcher() -> Html { let user = use_state(|| None::<User>); let loading = use_state(|| false); let error = use_state(|| None::<String>); let fetch_user = { let user = user.clone(); let loading = loading.clone(); let error = error.clone(); Callback::from(move |_| { loading.set(true); error.set(None); spawn_local(async move { let client = reqwest::Client::new(); match client .get("https://jsonplaceholder.typicode.com/users/1") .send() .await { Ok(response) => { match response.json::<User>().await { Ok(data) => { user.set(Some(data)); } Err(e) => { error.set(Some(format!("Parse error: {}", e))); } } } Err(e) => { error.set(Some(format!("Request error: {}", e))); } } loading.set(false); }); }) }; html! { <div> <button onclick={fetch_user} disabled={*loading}> { if *loading { "Loading..." } else { "Fetch User" } } </button> { if let Some(ref err) = *error { html! { <p class="error">{ err }</p> } } else if let Some(ref u) = *user { html! { <div class="user-card"> <h2>{ &u.name }</h2> <p>{ "ID: " }{ u.id }</p> <p>{ "Email: " }{ &u.email }</p> </div> } } else { html! { <p>{ "No user data" }</p> }} </div> } }

2. Using gloo-net for HTTP Requests

toml
[dependencies] gloo-net = { version = "0.4", features = ["http", "fetch"] } serde = { version = "1.0", features = ["derive"] }
rust
use gloo_net::http::Request; use serde::{Deserialize, Serialize}; use yew::prelude::*; #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Post { pub id: u32, pub title: String, pub body: String, } #[function_component(PostFetcher)] fn post_fetcher() -> Html { let posts = use_state(|| Vec::<Post>::new()); let loading = use_state(|| false); let fetch_posts = { let posts = posts.clone(); let loading = loading.clone(); Callback::from(move |_| { loading.set(true); spawn_local(async move { match Request::get("https://jsonplaceholder.typicode.com/posts") .send() .await { Ok(response) => { if response.ok() { match response.json::<Vec<Post>>().await { Ok(data) => { posts.set(data); } Err(e) => { web_sys::console::error_1(&format!("Parse error: {}", e).into()); } } } } Err(e) => { web_sys::console::error_1(&format!("Request error: {}", e).into()); } } loading.set(false); }); }) }; html! { <div> <button onclick={fetch_posts} disabled={*loading}> { if *loading { "Loading..." } else { "Fetch Posts" } } </button> <div class="posts"> { posts.iter().map(|post| { html! { <div key={post.id} class="post-card"> <h3>{ &post.title }</h3> <p>{ &post.body }</p> </div> } }).collect::<Html>() } </div> </div> } }

WebSocket Communication

1. Using gloo-net WebSocket

toml
[dependencies] gloo-net = { version = "0.4", features = ["websocket"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0"
rust
use gloo_net::websocket::{futures::WebSocket, Message, WebSocketError}; use serde::{Deserialize, Serialize}; use yew::prelude::*; #[derive(Debug, Serialize, Deserialize, Clone)] pub struct ChatMessage { pub id: String, pub text: String, pub timestamp: u64, } #[function_component(ChatApp)] fn chat_app() -> Html { let messages = use_state(|| Vec::<ChatMessage>::new()); let input_value = use_state(|| String::new()); let connected = use_state(|| false); let connect = { let messages = messages.clone(); let connected = connected.clone(); Callback::from(move |_| { let ws = WebSocket::open("wss://echo.websocket.org").unwrap(); let onmessage = { let messages = messages.clone(); Callback::from(move |msg: Message| { if let Message::Text(text) = msg { if let Ok(chat_msg) = serde_json::from_str::<ChatMessage>(&text) { let mut msgs = (*messages).clone(); msgs.push(chat_msg); messages.set(msgs); } } }) }; ws.set_onmessage(onmessage); connected.set(true); }) }; let send_message = { let input_value = input_value.clone(); let messages = messages.clone(); Callback::from(move |_| { if !input_value.is_empty() { let chat_msg = ChatMessage { id: uuid::Uuid::new_v4().to_string(), text: (*input_value).clone(), timestamp: chrono::Utc::now().timestamp() as u64, }; let mut msgs = (*messages).clone(); msgs.push(chat_msg.clone()); messages.set(msgs); input_value.set(String::new()); } }) }; let oninput = { 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="chat-app"> <div class="chat-header"> <h2>{ "Chat Room" }</h2> <button onclick={connect} disabled={*connected}> { if *connected { "Connected" } else { "Connect" } } </button> </div> <div class="messages"> { messages.iter().map(|msg| { html! { <div key={msg.id.clone()} class="message"> <span class="timestamp">{ msg.timestamp }</span> <span class="text">{ &msg.text }</span> </div> } }).collect::<Html>() } </div> <div class="input-area"> <input type="text" value={(*input_value).clone()} oninput={oninput} placeholder="Type a message..." /> <button onclick={send_message} disabled={input_value.is_empty()}> { "Send" } </button> </div> </div> } }

Error Handling and Retry Mechanism

1. Implementing Retry Logic

rust
use yew::prelude::*; #[function_component(RetryExample)] fn retry_example() -> Html { let data = use_state(|| None::<String>); let loading = use_state(|| false); let error = use_state(|| None::<String>); let retry_count = use_state(|| 0); let fetch_with_retry = { let data = data.clone(); let loading = loading.clone(); let error = error.clone(); let retry_count = retry_count.clone(); Callback::from(move |_| { loading.set(true); error.set(None); retry_count.set(*retry_count + 1); spawn_local(async move { let max_retries = 3; let mut attempt = 0; let mut result: Option<String> = None; while attempt < max_retries { attempt += 1; // Simulate potentially failing request gloo_timers::future::sleep(std::time::Duration::from_millis(500)).await; // Simulate 50% failure rate if rand::random::<bool>() { result = Some(format!("Success on attempt {}", attempt)); break; } } if let Some(value) = result { data.set(Some(value)); error.set(None); } else { error.set(Some("Max retries exceeded".to_string())); } loading.set(false); }); }) }; html! { <div> <button onclick={fetch_with_retry} disabled={*loading}> { if *loading { "Retrying..." } else { "Fetch with Retry" } } </button> <p>{ "Attempts: " }{ *retry_count }</p> { if let Some(ref err) = *error { html! { <p class="error">{ err }</p> } } else if let Some(ref value) = *data { html! { <p class="success">{ value }</p> } }} </div> } }

2. Using use_async Hook

rust
use yew::prelude::*; use yew_hooks::prelude::*; #[function_component(UseAsyncExample)] fn use_async_example() -> Html { let async_data = use_async(async { // Simulate async operation gloo_timers::future::sleep(std::time::Duration::from_secs(1)).await; Ok::<String, String>("Async operation completed!".to_string()) }); html! { <div> <button onclick={|_| async_data.run()} disabled={async_data.loading}> { if async_data.loading { "Running..." } else { "Run Async" } } </button> { match &async_data.data { Some(data) => html! { <p class="success">{ data }</p> }, None => html! { <p>{ "No data yet" }</p> }, }} { match &async_data.error { Some(error) => html! { <p class="error">{ error }</p> }, None => html! {}, }} </div> } }

Data Caching and State Management

1. Implementing Simple Data Caching

rust
use std::collections::HashMap; use yew::prelude::*; #[function_component(CachedData)] fn cached_data() -> Html { let cache = use_mut_ref(|| HashMap::<String, String>::new()); let data = use_state(|| None::<String>); let loading = use_state(|| false); let fetch_cached = { let cache = cache.clone(); let data = data.clone(); let loading = loading.clone(); Callback::from(move |key: String| { // Check cache if let Some(cached_value) = cache.borrow().get(&key) { data.set(Some(cached_value.clone())); return; } // Fetch from server loading.set(true); spawn_local(async move { // Simulate API call gloo_timers::future::sleep(std::time::Duration::from_secs(1)).await; let value = format!("Data for {}", key); // Update cache cache.borrow_mut().insert(key.clone(), value.clone()); data.set(Some(value)); loading.set(false); }); }) }; html! { <div> <button onclick={Callback::from(move |_| fetch_cached.emit("key1".to_string()))}> { "Fetch Key 1" } </button> <button onclick={Callback::from(move |_| fetch_cached.emit("key2".to_string()))}> { "Fetch Key 2" } </button> { if *loading { html! { <p>{ "Loading..." }</p> } } else if let Some(ref value) = *data { html! { <p>{ value }</p> } }} </div> } }

Best Practices

  1. Error Handling: Always handle errors in async operations
  2. Loading State: Provide clear loading feedback
  3. Cancellation: Implement mechanism to cancel incomplete requests
  4. Data Caching: Reasonably use caching to reduce unnecessary requests
  5. Retry Mechanism: Implement retry logic for critical operations
  6. Type Safety: Use Rust's type system to ensure data safety
标签:Yew