Fetching Data
In this chapter, we will fetch data from the hacker news API and use it to render the list of top posts in our application.
Defining the API
First we need to create some utilities to fetch data from the hackernews API using reqwest:
// Define the Hackernews API use futures::future::join_all; pub static BASE_API_URL: &str = "https://hacker-news.firebaseio.com/v0/"; pub static ITEM_API: &str = "item/"; pub static USER_API: &str = "user/"; const COMMENT_DEPTH: i64 = 2; pub async fn get_story_preview(id: i64) -> Result<StoryItem, reqwest::Error> { let url = format!("{}{}{}.json", BASE_API_URL, ITEM_API, id); reqwest::get(&url).await?.json().await } pub async fn get_stories(count: usize) -> Result<Vec<StoryItem>, reqwest::Error> { let url = format!("{}topstories.json", BASE_API_URL); let stories_ids = &reqwest::get(&url).await?.json::<Vec<i64>>().await?[..count]; let story_futures = stories_ids[..usize::min(stories_ids.len(), count)] .iter() .map(|&story_id| get_story_preview(story_id)); let stories = join_all(story_futures) .await .into_iter() .filter_map(|story| story.ok()) .collect(); Ok(stories) } pub async fn get_story(id: i64) -> Result<StoryPageData, reqwest::Error> { let url = format!("{}{}{}.json", BASE_API_URL, ITEM_API, id); let mut story = reqwest::get(&url).await?.json::<StoryPageData>().await?; let comment_futures = story.item.kids.iter().map(|&id| get_comment(id)); let comments = join_all(comment_futures) .await .into_iter() .filter_map(|c| c.ok()) .collect(); story.comments = comments; Ok(story) } #[async_recursion::async_recursion(?Send)] pub async fn get_comment_with_depth(id: i64, depth: i64) -> Result<Comment, reqwest::Error> { let url = format!("{}{}{}.json", BASE_API_URL, ITEM_API, id); let mut comment = reqwest::get(&url).await?.json::<Comment>().await?; if depth > 0 { let sub_comments_futures = comment .kids .iter() .map(|story_id| get_comment_with_depth(*story_id, depth - 1)); comment.sub_comments = join_all(sub_comments_futures) .await .into_iter() .filter_map(|c| c.ok()) .collect(); } Ok(comment) } pub async fn get_comment(comment_id: i64) -> Result<Comment, reqwest::Error> { let comment = get_comment_with_depth(comment_id, COMMENT_DEPTH).await?; Ok(comment) }
Working with Async
use_future
is a hook that lets you run an async closure, and provides you with its result.
For example, we can make an API request (using reqwest) inside use_future:
fn Stories(cx: Scope) -> Element { // Fetch the top 10 stories on Hackernews let stories = use_future(cx, (), |_| get_stories(10)); // check if the future is resolved match stories.value() { Some(Ok(list)) => { // if it is, render the stories render! { div { // iterate over the stories with a for loop for story in list { // render every story with the StoryListing component StoryListing { story: story.clone() } } } } } Some(Err(err)) => { // if there was an error, render the error render! {"An error occurred while fetching stories {err}"} } None => { // if the future is not resolved yet, render a loading message render! {"Loading items"} } } }
The code inside use_future
will be submitted to the Dioxus scheduler once the component has rendered.
We can use .value()
to get the result of the future. On the first run, since there's no data ready when the component loads, its value will be None
. However, once the future is finished, the component will be re-rendered and the value will now be Some(...)
, containing the return value of the closure.
We can then render the result by looping over each of the posts and rendering them with the StoryListing
component.
You can read more about working with Async in Dioxus in the Async reference
Lazily Fetching Data
Finally, we will lazily fetch the comments on each post as the user hovers over the post.
We need to revisit the code that handles hovering over an item. Instead of passing an empty list of comments, we can fetch all the related comments when the user hovers over the item.
We will cache the list of comments with a use_ref hook. This hook allows you to store some state in a single component. When the user triggers fetching the comments we will check if the response has already been cached before fetching the data from the hackernews API.
// New async fn resolve_story( full_story: UseRef<Option<StoryPageData>>, preview_state: UseSharedState<PreviewState>, story_id: i64, ) { if let Some(cached) = &*full_story.read() { *preview_state.write() = PreviewState::Loaded(cached.clone()); return; } *preview_state.write() = PreviewState::Loading; if let Ok(story) = get_story(story_id).await { *preview_state.write() = PreviewState::Loaded(story.clone()); *full_story.write() = Some(story); } } #[component] fn StoryListing(cx: Scope, story: StoryItem) -> Element { let preview_state = use_shared_state::<PreviewState>(cx).unwrap(); let StoryItem { title, url, by, score, time, kids, id, .. } = story; // New let full_story = use_ref(cx, || None); let url = url.as_deref().unwrap_or_default(); let hostname = url .trim_start_matches("https://") .trim_start_matches("http://") .trim_start_matches("www."); let score = format!("{score} {}", if *score == 1 { " point" } else { " points" }); let comments = format!( "{} {}", kids.len(), if kids.len() == 1 { " comment" } else { " comments" } ); let time = time.format("%D %l:%M %p"); cx.render(rsx! { div { padding: "0.5rem", position: "relative", onmouseenter: move |_event| { // New // If you return a future from an event handler, it will be run automatically resolve_story(full_story.clone(), preview_state.clone(), *id) }, div { font_size: "1.5rem", a { href: url, onfocus: move |_event| { // New resolve_story(full_story.clone(), preview_state.clone(), *id) }, // ...