Suspense and Asynchronous Loading
Modern applications often need to load data asynchronously from APIs, databases, or other sources. The Suspense
component in WaterUI provides an elegant way to handle async content loading while maintaining a responsive user interface. This chapter covers everything you need to know about implementing suspense in your applications.
Basic Suspense Usage
The simplest way to use Suspense is with async functions that return views:
#![allow(unused)] fn main() { use waterui::{View}; use waterui_text::text; use waterui::component::layout::stack::{vstack}; use waterui::widget::suspense::Suspense; // Async function that loads data async fn load_user_profile(user_id: u32) -> impl View { // Simulate API call tokio::time::sleep(Duration::from_secs(2)).await; let user_data = fetch_user_data(user_id).await; vstack(( text!("Name: {}", user_data.name), text!("Email: {}", user_data.email), text!("Joined: {}", user_data.joined_date), )) } fn user_profile_view(user_id: u32) -> impl View { // Basic suspense with custom loading view Suspense::new(load_user_profile(user_id)) .loading(text!("Loading user profile...")) } }
Using Default Loading Views
You can set up default loading views in your application's environment:
#![allow(unused)] fn main() { use waterui::{Environment}; use waterui::view::AnyViewBuilder; use waterui::widget::suspense::{Suspense, DefaultLoadingView}; use waterui_text::text; use waterui::component::layout::stack::vstack; use waterui::component::layout::{Edge, Frame}; // Set up default loading view in your app fn setup_app_environment() -> Environment { let loading_view = AnyViewBuilder::new(|_| { vstack(( text!("Loading..."), )) .frame(Frame::new().margin(Edge::round(20.0))) }); Environment::new().with(DefaultLoadingView(loading_view)) } // Components can now use the default loading view fn simple_async_view() -> impl View { Suspense::new(load_data) // Uses default loading view from environment } }
The SuspendedView Trait
Any type can be used with Suspense by implementing the SuspendedView
trait. The trait is automatically implemented for any Future
that resolves to a View
.
Automatic Implementation for Futures
#![allow(unused)] fn main() { use waterui::widget::suspense::{Suspense, SuspendedView}; use waterui_text::text; // These all work with Suspense automatically: // 1. Async functions async fn fetch_weather() -> impl View { let weather = get_weather_data().await; text!("Temperature: {}°F", weather.temperature) } // 2. Async closures let load_news = async move || { let articles = fetch_news_articles().await; news_list_view(articles) }; // 3. Future types use std::future::Future; use std::pin::Pin; type BoxedFuture = Pin<Box<dyn Future<Output = impl View>>>; fn get_async_content() -> BoxedFuture { Box::pin(async { text!("Async content loaded!") }) } // All work with Suspense: let weather_view = Suspense::new(fetch_weather); let news_view = Suspense::new(load_news); let content_view = Suspense::new(get_async_content()); }
Custom SuspendedView Implementation
For more complex scenarios, you can implement SuspendedView
manually:
#![allow(unused)] fn main() { use waterui::{Environment, View}; use waterui::widget::suspense::SuspendedView; use waterui_text::text; use waterui::component::layout::stack::vstack; struct DataLoader { user_id: u32, include_posts: bool, } impl SuspendedView for DataLoader { async fn body(self, _env: Environment) -> impl View { // Custom loading logic with environment access let user = fetch_user(self.user_id).await; if self.include_posts { let posts = fetch_user_posts(self.user_id).await; vstack(( user_profile_view(user), posts_list_view(posts), )) } else { user_profile_view(user) } } } // Usage fn user_dashboard(user_id: u32, show_posts: bool) -> impl View { Suspense::new(DataLoader { user_id, include_posts: show_posts, }) .loading(text!("Loading dashboard...")) } }