Hooks and component state

So far, our components have had no state like a normal Rust function. However, in a UI component, it is often useful to have stateful functionality to build user interactions. For example, you might want to track whether the user has opened a drop-down and render different things accordingly.

Hooks allow us to create state in our components. Hooks are Rust functions you call in a constant order in a component that add additional functionality to the component.

Dioxus provides many built-in hooks, but if those hooks don't fit your specific use case, you also can create your own hook

use_signal hook

use_signal is one of the simplest hooks.

  • You provide a closure that determines the initial value: let mut count = use_signal(|| 0);
  • use_signal gives you the current value, and a way to write to the value
  • When the value updates, use_signal makes the component re-render (along with any other component that references it), and then provides you with the new value.

For example, you might have seen the counter example, in which state (a number) is tracked using the use_signal hook:

src/hooks_counter.rs
pub fn App() -> Element {
    // count will be initialized to 0 the first time the component is rendered
    let mut count = use_signal(|| 0);

    rsx! {
        h1 { "High-Five counter: {count}" }
        button { onclick: move |_| count += 1, "Up high!" }
        button { onclick: move |_| count -= 1, "Down low!" }
    }
}

High-Five counter: 0

Every time the component's state changes, it re-renders, and the component function is called, so you can describe what you want the new UI to look like. You don't have to worry about "changing" anything – describe what you want in terms of the state, and Dioxus will take care of the rest!

use_signal returns your value wrapped in a smart pointer of type Signal that is Copy. This is why you can both read the value and update it, even within an event handler.

You can use multiple hooks in the same component if you want:

src/hooks_counter_two_state.rs
pub fn App() -> Element {
    let mut count_a = use_signal(|| 0);
    let mut count_b = use_signal(|| 0);

    rsx! {
        h1 { "Counter_a: {count_a}" }
        button { onclick: move |_| count_a += 1, "a++" }
        button { onclick: move |_| count_a -= 1, "a--" }
        h1 { "Counter_b: {count_b}" }
        button { onclick: move |_| count_b += 1, "b++" }
        button { onclick: move |_| count_b -= 1, "b--" }
    }
}

Counter_a: 0

Counter_b: 0

You can also use use_signal to store more complex state, like a Vec. You can read and write to the state with the read and write methods:

src/hooks_use_signal.rs
pub fn App() -> Element {
    let mut list = use_signal(Vec::new);

    rsx! {
        p { "Current list: {list:?}" }
        button {
            onclick: move |event| {
                let list_len = list.len();
                list.push(list_len);
                list.push(list_len);
            },
            "Add two elements!"
        }
    }
}

Current list: []

Rules of hooks

The above example might seem a bit magic since Rust functions are typically not associated with state. Dioxus allows hooks to maintain state across renders through a hidden scope that is associated with the component.

But how can Dioxus differentiate between multiple hooks in the same component? As you saw in the second example, both use_signal functions were called with the same parameters, so how come they can return different things when the counters are different?

src/hooks_counter_two_state.rs
let mut count_a = use_signal(|| 0);
let mut count_b = use_signal(|| 0);

This is only possible because the two hooks are always called in the same order, so Dioxus knows which is which. Because the order you call hooks matters, you must follow certain rules when using hooks:

  1. Hooks may be only used in components or other hooks (we'll get to that later).
  2. On every call to a component function.
  3. The same hooks must be called (except in the case of early returns, as explained later in the Error Handling chapter).
  4. In the same order.
  5. Hook names should start with use_ so you don't accidentally confuse them with regularuse_signal(), use_signal(), use_resource(), etc...).

These rules mean that there are certain things you can't do with hooks:

No hooks in conditionals

src/hooks_bad.rs
// ❌ 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_signal(|| "hands");
    println!("clap your {something}")
}

// ✅ instead, *always* call use_signal
// You can put other stuff in the conditional though
let something = use_signal(|| "hands");
if you_are_happy && you_know_it {
    println!("clap your {something}")
}

No hooks in closures

src/hooks_bad.rs
// ❌ 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_signal(|| 0);
    b()
};

// ✅ instead, move hook `b` outside
let b = use_signal(|| 0);
let _a = || b();

No hooks in loops

src/hooks_bad.rs
// `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_signal(|| false);
    println!("selected: {is_selected}");
}

// ✅ Instead, use a hashmap with use_signal
let selection_map = use_signal(HashMap::<&str, bool>::new);

for name in &names {
    let is_selected = selection_map.read()[name];
    println!("selected: {is_selected}");
}

Additional resources