Interactivity
In this chapter, we will add a preview for articles you hover over or links you focus on.
Creating a Preview
First, let's split our app into a Stories component on the left side of the screen, and a preview component on the right side of the screen:
pub fn App(cx: Scope) -> Element { cx.render(rsx! { div { display: "flex", flex_direction: "row", width: "100%", div { width: "50%", Stories {} } div { width: "50%", Preview {} } } }) } // New fn Stories(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(), } } } } // New #[derive(Clone, Debug)] enum PreviewState { Unset, Loading, Loaded(StoryPageData), } // New fn Preview(cx: Scope) -> Element { let preview_state = PreviewState::Unset; match preview_state { 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() } } } } } } } // NEW #[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() } } } } }
Event Handlers
Next, we need to detect when the user hovers over a section or focuses a link. We can use an event listener to listen for the hover and focus events.
Event handlers are similar to regular attributes, but their name usually starts with on
- and they accept closures as values. The closure will be called whenever the event it listens for is triggered. When an event is triggered, information about the event is passed to the closure though the Event structure.
Let's create a onmouseenter
event listener in the StoryListing
component:
cx.render(rsx! { div { padding: "0.5rem", position: "relative", onmouseenter: move |_| { // NEW }, div { font_size: "1.5rem", a { href: url, onfocus: move |_event| { // NEW }, "{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}" } } } })
You can read more about Event Handlers in the Event Handler reference
State
So far our components have had no state like normal rust functions. To make our application change when we hover over a link we need state to store the currently hovered link in the root of the application.
You can create state in dioxus using hooks. Hooks are Rust functions that take a reference to ScopeState
(in a component, you can pass cx
), and provide you with functionality and state.
In this case, we will use the use_shared_state_provider
and use_shared_state
hooks:
- You can provide a closure to
use_shared_state_provider
that determines the initial value of the shared state and provides the value to all child components - You can then use the
use_shared_state
hook to read and modify that state in thePreview
andStoryListing
components - When the value updates,
use_shared_state
will make the component re-render, and provides you with the new value
Note: You should prefer local state hooks like use_state or use_ref when you only use state in one component. Because we use state in multiple components, we can use a global state pattern
pub fn App(cx: Scope) -> Element { use_shared_state_provider(cx, || PreviewState::Unset);
#[component] fn StoryListing(cx: Scope, story: StoryItem) -> Element { // New let preview_state = use_shared_state::<PreviewState>(cx).unwrap(); let StoryItem { title, url, by, score, time, kids, .. } = 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", onmouseenter: move |_event| { // NEW // set the preview state to this story *preview_state.write() = PreviewState::Loaded(StoryPageData { item: story.clone(), comments: vec![], }); }, div { font_size: "1.5rem", a { href: url, onfocus: move |_event| { // NEW // set the preview state to this story *preview_state.write() = PreviewState::Loaded(StoryPageData { item: story.clone(), comments: vec![], }); },
fn Preview(cx: Scope) -> Element { // New let preview_state = use_shared_state::<PreviewState>(cx)?; // New match &*preview_state.read() {
You can read more about Hooks in the Hooks reference
The Rules of Hooks
Hooks are a powerful way to manage state in Dioxus, but there are some rules you need to follow to insure they work as expected. Dioxus uses the order you call hooks to differentiate between hooks. Because the order you call hooks matters, you must follow these rules:
- Hooks may be only used in components or other hooks (we'll get to that later)
- On every call to the component function
- The same hooks must be called
- In the same order
- Hooks name's should start with
use_
so you don't accidentally confuse them with regular functions
These rules mean that there are certain things you can't do with hooks:
No Hooks in Conditionals
// ❌ don't call hooks in conditionals! // We must ensure that the same hooks will be called every time // But `if` statements only run if the conditional is true! // So we might violate rule 2. if you_are_happy && you_know_it { let something = use_state(cx, || "hands"); println!("clap your {something}") } // ✅ instead, *always* call use_state // You can put other stuff in the conditional though let something = use_state(cx, || "hands"); if you_are_happy && you_know_it { println!("clap your {something}") }
No Hooks in Closures
// ❌ don't call hooks inside closures! // We can't guarantee that the closure, if used, will be called in the same order every time let _a = || { let b = use_state(cx, || 0); b.get() }; // ✅ instead, move hook `b` outside let b = use_state(cx, || 0); let _a = || b.get();
No Hooks in Loops
// `names` is a Vec<&str> // ❌ Do not use hooks in loops! // In this case, if the length of the Vec changes, we break rule 2 for _name in &names { let is_selected = use_state(cx, || false); println!("selected: {is_selected}"); } // ✅ Instead, use a hashmap with use_ref let selection_map = use_ref(cx, HashMap::<&str, bool>::new); for name in &names { let is_selected = selection_map.read()[name]; println!("selected: {is_selected}"); }