Search code examples
rustyew

Mutating Variable inside a Yew Callback


Working forward from the last example in this page of the Yew Docs, I'd like to use a callback to update the label on an input, rather than just outputting a console message. Or in other words, having done what the example does successfully, I'd then like to change something outside the closure, to wit, a string which can be used in the html later.

#[function_component(Settings)]
pub fn settings_page() -> Html {
    let mut value_label = String::new();

    let onchange_slider = {
        Callback::from(move |e: Event| {
            let input: HtmlInputElement = e.target_unchecked_into();
            let value = input.value().parse::<i32>().unwrap_or(10);
            log::info!("value = {}", value);  //This works fine.
            value_label = format!("The value is {}", value); //Error: closure is FnMut because it mutates the variable value_label here
        })
};

html! {
    <div class="slider">
         <input type="range" class="form-range" min="-5" max="20" id="my_broken_slider" onchange={onchange_slider.clone()} />
          <label for="my_broken_slider" class="form-label">{value_label}</label>
    </div>
}
}

It seems like the docs are almost useful here. I can definitely call the onchange function, and I can read the slider value. The trick appears to be, from the docs, that Callback is "just an Fn wrapped in Rc," which is neat for cheap cloning, but less neat from the perspective of doing something useful on a web page. So it's as if, by design, the callback isn't allowed to change anything. That makes me think there's some actually correct way to do what I need to do that's unrelated to callbacks entirely, but ... I'm at a loss here.


Solution

  • Even if you would be able to change value_label (for example, by using interior mutability), this would still not cause the necessary re-render. You need to use state:

    #[function_component(Settings)]
    pub fn settings_page() -> Html {
        let value_label = use_state(String::new);
    
        let onchange_slider = {
            let value_label = value_label.clone();
            Callback::from(move |e: Event| {
                let input: HtmlInputElement = e.target_unchecked_into();
                let value = input.value().parse::<i32>().unwrap_or(10);
                log::info!("value = {}", &*value);
                value_label.set(format!("The value is {}", value));
            })
        };
    
        html! {
            <div class="slider">
                 <input type="range" class="form-range" min="-5" max="20" id="my_broken_slider" onchange={onchange_slider.clone()} />
                  <label for="my_broken_slider" class="form-label">{&*value_label}</label>
            </div>
        }
    }