Estado de compartilhamento

Muitas vezes, vários componentes precisam acessar o mesmo estado. Dependendo de suas necessidades, existem várias maneiras de implementar isso.

Elevenado o Estado

Uma abordagem para compartilhar o estado entre os componentes é "elevá-lo" até o ancestral comum mais próximo. Isso significa colocar o hook use_state em um componente pai e passar os valores necessários como props.

Por exemplo, suponha que queremos construir um editor de memes. Queremos ter uma entrada para editar a legenda do meme, mas também uma visualização do meme com a legenda. Logicamente, o meme e a entrada são 2 componentes separados, mas precisam acessar o mesmo estado (a legenda atual).

Claro, neste exemplo simples, poderíamos escrever tudo em um componente - mas é melhor dividir tudo em componentes menores para tornar o código mais reutilizável e fácil de manter (isso é ainda mais importante para aplicativos maiores e complexos) .

Começamos com um componente Meme, responsável por renderizar um meme com uma determinada legenda:


#![allow(unused)]
fn main() {
#[inline_props]
fn Meme<'a>(cx: Scope<'a>, caption: &'a str) -> Element<'a> {
    let container_style = r#"
        position: relative;
        width: fit-content;
    "#;

    let caption_container_style = r#"
        position: absolute;
        bottom: 0;
        left: 0;
        right: 0;
        padding: 16px 8px;
    "#;

    let caption_style = r"
        font-size: 32px;
        margin: 0;
        color: white;
        text-align: center;
    ";

    cx.render(rsx!(
        div {
            style: "{container_style}",
            img {
                src: "https://i.imgflip.com/2zh47r.jpg",
                height: "500px",
            },
            div {
                style: "{caption_container_style}",
                p {
                    style: "{caption_style}",
                    "{caption}"
                }
            }
        }
    ))
}
}

Observe que o componente Meme não sabe de onde vem a legenda – ela pode ser armazenada em use_state, use_ref ou uma constante. Isso garante que seja muito reutilizável - o mesmo componente pode ser usado para uma galeria de memes sem nenhuma alteração!

Também criamos um editor de legendas, totalmente desacoplado do meme. O editor de legendas não deve armazenar a legenda em si – caso contrário, como iremos fornecê-la ao componente Meme? Em vez disso, ele deve aceitar a legenda atual como um suporte, bem como um manipulador de eventos para delegar eventos de entrada para:


#![allow(unused)]
fn main() {
#[inline_props]
fn CaptionEditor<'a>(
    cx: Scope<'a>,
    caption: &'a str,
    on_input: EventHandler<'a, FormEvent>,
) -> Element<'a> {
    let input_style = r"
        border: none;
        background: cornflowerblue;
        padding: 8px 16px;
        margin: 0;
        border-radius: 4px;
        color: white;
    ";

    cx.render(rsx!(input {
        style: "{input_style}",
        value: "{caption}",
        oninput: move |event| on_input.call(event),
    }))
}
}

Finalmente, um terceiro componente renderizará os outros dois como filhos. Ele será responsável por manter o estado e passar os props relevantes.


#![allow(unused)]
fn main() {
fn MemeEditor(cx: Scope) -> Element {
    let container_style = r"
        display: flex;
        flex-direction: column;
        gap: 16px;
        margin: 0 auto;
        width: fit-content;
    ";

    let caption = use_state(cx, || "me waiting for my rust code to compile".to_string());

    cx.render(rsx! {
        div {
            style: "{container_style}",
            h1 { "Meme Editor" },
            Meme {
                caption: caption,
            },
            CaptionEditor {
                caption: caption,
                on_input: move |event: FormEvent| {caption.set(event.value.clone());},
            },
        }
    })
}
}

Captura de tela do editor de memes: Um velho esqueleto de plástico sentado em um banco de parque. Legenda: "eu esperando por um recurso de idioma"

Usando o contexto

Às vezes, algum estado precisa ser compartilhado entre vários componentes na árvore, e passá-lo pelos props é muito inconveniente.

Suponha agora que queremos implementar uma alternância de modo escuro para nosso aplicativo. Para conseguir isso, faremos com que cada componente selecione o estilo dependendo se o modo escuro está ativado ou não.

Nota: estamos escolhendo esta abordagem como exemplo. Existem maneiras melhores de implementar o modo escuro (por exemplo, usando variáveis ​​CSS). Vamos fingir que as variáveis ​​CSS não existem – bem-vindo a 2013!

Agora, poderíamos escrever outro use_state no componente superior e passar is_dark_mode para cada componente através de props. Mas pense no que acontecerá à medida que o aplicativo crescer em complexidade – quase todos os componentes que renderizam qualquer CSS precisarão saber se o modo escuro está ativado ou não – para que todos precisem do mesmo suporte do modo escuro. E cada componente pai precisará passá-lo para eles. Imagine como isso ficaria confuso e verboso, especialmente se tivéssemos componentes com vários níveis de profundidade!

A Dioxus oferece uma solução melhor do que esta "perfuração com hélice" – fornecendo contexto. O hook use_context_provider é semelhante ao use_ref, mas o torna disponível através do use_context para todos os componentes filhos.

Primeiro, temos que criar um struct para nossa configuração de modo escuro:


#![allow(unused)]
fn main() {
struct DarkMode(bool);
}

Agora, em um componente de nível superior (como App), podemos fornecer o contexto DarkMode para todos os componentes filhos:


#![allow(unused)]
fn main() {
use_shared_state_provider(cx, || DarkMode(false));
}

Como resultado, qualquer componente filho de App (direto ou não), pode acessar o contexto DarkMode.


#![allow(unused)]
fn main() {
let dark_mode_context = use_shared_state::<DarkMode>(cx);
}

use_context retorna Option<UseSharedState<DarkMode>> aqui. Se o contexto foi fornecido, o valor é Some(UseSharedState), que você pode chamar .read ou .write, similarmente a UseRef. Caso contrário, o valor é None.

Por exemplo, aqui está como implementaríamos a alternância do modo escuro, que lê o contexto (para determinar a cor que deve renderizar) e grava nele (para alternar o modo escuro):


#![allow(unused)]
fn main() {
pub fn DarkModeToggle(cx: Scope) -> Element {
    let dark_mode = use_shared_state::<DarkMode>(cx).unwrap();

    let style = if dark_mode.read().0 {
        "color:white"
    } else {
        ""
    };

    cx.render(rsx!(label {
        style: "{style}",
        "Dark Mode",
        input {
            r#type: "checkbox",
            oninput: move |event| {
                let is_enabled = event.value == "true";
                dark_mode.write().0 = is_enabled;
            },
        },
    }))
}
}