Hooks e Estado do Componente

Até agora nossos componentes, sendo funções Rust, não tinham estado – eles estavam sempre renderizando a mesma coisa. No entanto, em um componente de interface do usuário, geralmente é útil ter uma funcionalidade com estado para criar interações do usuário. Por exemplo, você pode querer rastrear se o usuário abriu uma lista suspensa e renderizar coisas diferentes de acordo.

Para lógica com estado, você pode usar hooks. Hooks são funções Rust que fazem referência a ScopeState (em um componente, você pode passar &cx), e fornecem funcionalidade e estado.

Hook use_state

use_state é um dos hooks mais simples.

  • Você fornece um fechamento que determina o valor inicial
  • use_state fornece o valor atual e uma maneira de atualizá-lo, definindo-o para outra coisa
  • Quando o valor é atualizado, use_state faz o componente renderizar novamente e fornece o novo valor

Por exemplo, você pode ter visto o exemplo do contador, no qual o estado (um número) é rastreado usando o hook use_state:


#![allow(unused)]
fn main() {
fn App(cx: Scope) -> Element {
    // count will be initialized to 0 the first time the component is rendered
    let mut count = use_state(cx, || 0);

    cx.render(rsx!(
        h1 { "High-Five counter: {count}" }
        button {
            onclick: move |_| {
                // changing the count will cause the component to re-render
                count += 1
            },
            "Up high!"
        }
        button {
            onclick: move |_| {
                // changing the count will cause the component to re-render
                count -= 1
            },
            "Down low!"
        }
    ))
}
}

Screenshot: counter app

Toda vez que o estado do componente muda, ele é renderizado novamente e a função do componente é chamada, para que você possa descrever como deseja que a nova interface do usuário se pareça. Você não precisa se preocupar em "mudar" nada - apenas descreva o que você quer em termos de estado, e Dioxus cuidará do resto!

use_state retorna seu valor envolto em uma smart pointer do tipo UseState. É por isso que você pode ler o valor e atualizá-lo, mesmo dentro de um manipulador.

Você pode usar vários hooks no mesmo componente se quiser:


#![allow(unused)]
fn main() {
fn App(cx: Scope) -> Element {
    let mut count_a = use_state(cx, || 0);
    let mut count_b = use_state(cx, || 0);

    cx.render(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--" }
    ))
}
}

Screenshot: app with two counters

Regras dos Hooks

O exemplo acima pode parecer um pouco mágico, já que as funções Rust normalmente não estão associadas ao estado. O Dioxus permite que os hooks mantenham o estado entre as renderizações através de uma referência a ScopeState, e é por isso que você deve passar &cx para eles.

Mas como Dioxus pode diferenciar entre vários hooks no mesmo componente? Como você viu no segundo exemplo, ambas as funções use_state foram chamadas com os mesmos parâmetros, então como elas podem retornar coisas diferentes quando os contadores são diferentes?


#![allow(unused)]
fn main() {
    let mut count_a = use_state(cx, || 0);
    let mut count_b = use_state(cx, || 0);
}

Isso só é possível porque os dois hooks são sempre chamados na mesma ordem, então Dioxus sabe qual é qual. Portanto, a ordem em que você chama os hooks é importante, e é por isso que você deve seguir certas regras ao usar os hooks:

  1. Hooks só podem ser usados em componentes ou outros hooks (falaremos disso mais tarde)
  2. Em cada chamada para a função componente
    1. Os mesmos hooks devem ser chamados
    2. Na mesma ordem
  3. Os nomes dos hooks devem começar com use_ para que você não os confunda acidentalmente com funções normais

Essas regras significam que há certas coisas que você não pode fazer com hooks:

Sem Hooks em Condicionais


#![allow(unused)]
fn main() {
// ❌ 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}")
}
}

Sem Hooks em Closures


#![allow(unused)]
fn main() {
// ❌ 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();
}

Sem Hooks em Loops


#![allow(unused)]
fn main() {
// `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}");
}
}

Gancho use_ref

use_state é ótimo para rastrear valores simples. No entanto, você pode notar na UseState API que a única maneira de modificar seu valor é substituí-lo por algo else (por exemplo, chamando set, ou através de um dos operadores +=, -=). Isso funciona bem quando é barato construir um valor (como qualquer primitivo). Mas e se você quiser manter dados mais complexos no estado dos componentes?

Por exemplo, suponha que queremos manter um Vec de valores. Se o armazenamos com use_state, a única maneira de adicionar um novo valor à lista seria criar um novo Vec com o valor adicional e colocá-lo no estado. Isto é custoso! Queremos modificar o Vec existente.

Felizmente, existe outro hook para isso, use_ref! É semelhante ao use_state, mas permite obter uma referência mutável aos dados contidos.

Aqui está um exemplo simples que mantém uma lista de eventos em um use_ref. Podemos adquirir acesso de escrita ao estado com .write(), e então apenas .push um novo valor para o estado:


#![allow(unused)]
fn main() {
fn App(cx: Scope) -> Element {
    let list = use_ref(cx, Vec::new);

    cx.render(rsx!(
        p { "Current list: {list.read():?}" }
        button {
            onclick: move |event| {
                list.with_mut(|list| list.push(event));
            },
            "Click me!"
        }
    ))
}
}

Os valores de retorno de use_state e use_ref, (UseState e UseRef, respectivamente) são de alguma forma semelhantes a [Cell](https://doc.rust-lang.org/std/ cell/) e RefCell – eles fornecem mutabilidade interior. No entanto, esses wrappers do Dioxus também garantem que o componente seja renderizado novamente sempre que você alterar o estado.