Conclusion
Well done! You've completed the Dioxus guide and built a hackernews application in Dioxus.
To continue your journey, you can attempt a challenge listed below, or look at the Dioxus reference.
Challenges
- Organize your components into separate files for better maintainability.
- Give your app some style if you haven't already.
- Integrate your application with the Dioxus router.
The full code for the hacker news project
src/hackernews_complete.rs
#![allow(non_snake_case)] use dioxus::prelude::*; pub fn App(cx: Scope) -> Element { use_shared_state_provider(cx, || PreviewState::Unset); cx.render(rsx! { div { display: "flex", flex_direction: "row", width: "100%", div { width: "50%", Stories {} } div { width: "50%", Preview {} } } }) } fn Stories(cx: Scope) -> Element { let story = use_future(cx, (), |_| get_stories(10)); match story.value() { Some(Ok(list)) => render! { div { for story in list { StoryListing { story: story.clone() } } } }, Some(Err(err)) => render! {"An error occurred while fetching stories {err}"}, None => render! {"Loading items"}, } } 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; 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| { resolve_story(full_story.clone(), preview_state.clone(), *id) }, div { font_size: "1.5rem", a { href: url, onfocus: move |_event| { resolve_story(full_story.clone(), preview_state.clone(), *id) }, "{title}" } a { color: "gray", href: "https://news.ycombinator.com/from?site={hostname}", text_decoration: "none", " ({hostname})" } } div { display: "flex", flex_direction: "row", color: "gray", div { "{score}" } div { padding_left: "0.5rem", "by {by}" } div { padding_left: "0.5rem", "{time}" } div { padding_left: "0.5rem", "{comments}" } } } }) } #[derive(Clone, Debug)] enum PreviewState { Unset, Loading, Loaded(StoryPageData), } fn Preview(cx: Scope) -> Element { let preview_state = use_shared_state::<PreviewState>(cx)?; match &*preview_state.read() { PreviewState::Unset => render! { "Hover over a story to preview it here" }, PreviewState::Loading => render! { "Loading..." }, PreviewState::Loaded(story) => { let title = &story.item.title; let url = story.item.url.as_deref().unwrap_or_default(); let text = story.item.text.as_deref().unwrap_or_default(); render! { div { padding: "0.5rem", div { font_size: "1.5rem", a { href: "{url}", "{title}" } } div { dangerous_inner_html: "{text}", } for comment in &story.comments { Comment { comment: comment.clone() } } } } } } } #[component] fn Comment(cx: Scope, comment: Comment) -> Element<'a> { render! { div { padding: "0.5rem", div { color: "gray", "by {comment.by}" } div { dangerous_inner_html: "{comment.text}" } for kid in &comment.sub_comments { Comment { comment: kid.clone() } } } } } // Define the Hackernews API and types use chrono::{DateTime, Utc}; use futures::future::join_all; use serde::{Deserialize, Serialize}; 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)); Ok(join_all(story_futures) .await .into_iter() .filter_map(|story| story.ok()) .collect()) } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct StoryPageData { #[serde(flatten)] pub item: StoryItem, #[serde(default)] pub comments: Vec<Comment>, } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct Comment { pub id: i64, /// there will be no by field if the comment was deleted #[serde(default)] pub by: String, #[serde(default)] pub text: String, #[serde(with = "chrono::serde::ts_seconds")] pub time: DateTime<Utc>, #[serde(default)] pub kids: Vec<i64>, #[serde(default)] pub sub_comments: Vec<Comment>, pub r#type: String, } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct StoryItem { pub id: i64, pub title: String, pub url: Option<String>, pub text: Option<String>, #[serde(default)] pub by: String, #[serde(default)] pub score: i64, #[serde(default)] pub descendants: i64, #[serde(with = "chrono::serde::ts_seconds")] pub time: DateTime<Utc>, #[serde(default)] pub kids: Vec<i64>, pub r#type: String, } 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> { get_comment_with_depth(comment_id, COMMENT_DEPTH).await }