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!" } )) } }
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 tipoUseState
. É 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--" } )) } }
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:
- Hooks só podem ser usados em componentes ou outros hooks (falaremos disso mais tarde)
- Em cada chamada para a função componente
- Os mesmos hooks devem ser chamados
- Na mesma ordem
- 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
euse_ref
, (UseState
eUseRef
, respectivamente) são de alguma forma semelhantes a [Cell
](https://doc.rust-lang.org/std/ cell/) eRefCell
– 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.