Your First Component
This chapter will teach you how to create a Component that displays a link to a post on hackernews.
First, let's define how to display a post. Dioxus is a declarative framework. This means that instead of telling Dioxus what to do (e.g. to "create an element" or "set the color to red") we simply declare how we want the UI to look.
To declare what you want your UI to look like, you will need to use the rsx
macro. Let's modify the rsx macro in the App
function from the getting started to show information about our story:
pub fn App(cx: Scope) -> Element { render! { "story" } }
If you run your application you should see something like this:
RSX mirrors HTML. Because of this you will need to know some html to use Dioxus.
Here are some resources to help get you started learning HTML:
In addition to HTML, Dioxus uses CSS to style applications. You can either use traditional CSS (what this guide uses) or use a tool like tailwind CSS:
- MDN Traditional CSS Guide
- W3 Schools Traditional CSS Tutorial
- Tailwind tutorial (used with the Tailwind setup example)
If you have existing html code, you can use the translate command to convert it to RSX. Or if you prefer to write html, you can use the html! macro to write html directly in your code.
Dynamic Text
Let's expand our App
component to include the story title, author, score, time posted, and number of comments. We can insert dynamic text in the render macro by inserting variables inside {}
s (this works similarly to the formatting in the println! macro):
pub fn App(cx: Scope) -> Element { let title = "title"; let by = "author"; let score = 0; let time = chrono::Utc::now(); let comments = "comments"; render! { "{title} by {by} ({score}) {time} {comments}" } }
Creating Elements
Next, let's wrap our post description in a div
. You can create HTML elements in Dioxus by putting a {
after the element name and a }
after the last child of the element:
pub fn App(cx: Scope) -> Element { let title = "title"; let by = "author"; let score = 0; let time = chrono::Utc::now(); let comments = "comments"; render! { div { "{title} by {by} ({score}) {time} {comments}" } } }
You can read more about elements in the rsx reference.
Setting Attributes
Next, let's add some padding around our post listing with an attribute.
Attributes (and listeners) modify the behavior or appearance of the element they are attached to. They are specified inside the {}
brackets before any children, using the name: value
syntax. You can format the text in the attribute as you would with a text node:
pub fn App(cx: Scope) -> Element { let title = "title"; let by = "author"; let score = 0; let time = chrono::Utc::now(); let comments = "comments"; render! { div { padding: "0.5rem", position: "relative", "{title} by {by} ({score}) {time} {comments}" } } }
Note: All attributes defined in
dioxus-html
follow the snake_case naming convention. They transform theirsnake_case
names to HTML'scamelCase
attributes.
Note: Styles can be used directly outside of the
style:
attribute. In the above example,color: "red"
is turned intostyle="color: red"
.
You can read more about elements in the attribute reference
Creating a Component
Just like you wouldn't want to write a complex program in a single, long, main
function, you shouldn't build a complex UI in a single App
function. Instead, you should break down the functionality of an app in logical parts called components.
A component is a Rust function, named in UpperCamelCase, that takes a Scope
parameter and returns an Element
describing the UI it wants to render. In fact, our App
function is a component!
Let's pull our story description into a new component:
fn StoryListing(cx: Scope) -> Element { let title = "title"; let by = "author"; let score = 0; let time = chrono::Utc::now(); let comments = "comments"; render! { div { padding: "0.5rem", position: "relative", "{title} by {by} ({score}) {time} {comments}" } } }
We can render our component like we would an element by putting {}
s after the component name. Let's modify our App
component to render our new StoryListing component:
pub fn App(cx: Scope) -> Element { render! { StoryListing { } } }
You can read more about elements in the component reference
Creating Props
Just like you can pass arguments to a function or attributes to an element, you can pass props to a component that customize its behavior!
We can define arguments that components can take when they are rendered (called Props
) by adding the #[component]
macro before our function definition and adding extra function arguments.
Currently, our StoryListing
component always renders the same story. We can modify it to accept a story to render as a prop.
We will also define what a post is and include information for how to transform our post to and from a different format using serde. This will be used with the hackernews API in a later chapter:
use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; // Define the Hackernews types #[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, } #[component] fn StoryListing(cx: Scope, story: StoryItem) -> Element { let StoryItem { title, by, score, time, kids, .. } = story; let comments = kids.len(); render! { div { padding: "0.5rem", position: "relative", "{title} by {by} ({score}) {time} {comments}" } } }
Now, let's modify the App
component to pass the story to our StoryListing
component like we would set an attribute on an element:
pub fn App(cx: Scope) -> Element { render! { StoryListing { story: StoryItem { id: 0, title: "hello hackernews".to_string(), url: None, text: None, by: "Author".to_string(), score: 0, descendants: 0, time: chrono::Utc::now(), kids: vec![], r#type: "".to_string(), } } } }
You can read more about Props in the Props reference
Cleaning Up Our Interface
Finally, by combining elements and attributes, we can make our post listing much more appealing:
Full code up to this point:
use dioxus::prelude::*; // Define the Hackernews types use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; #[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 fn App(cx: Scope) -> Element { render! { StoryListing { story: StoryItem { id: 0, title: "hello hackernews".to_string(), url: None, text: None, by: "Author".to_string(), score: 0, descendants: 0, time: Utc::now(), kids: vec![], r#type: "".to_string(), } } } } #[component] fn StoryListing(cx: Scope, story: StoryItem) -> Element { let StoryItem { title, url, by, score, time, kids, id, .. } = story; 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", div { font_size: "1.5rem", a { href: url, "{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}" } } } }) }