Search code examples
ruststateyew

How to set state inside listener in use_effect_with_deps?


I have an event listener inside an use_effect, there I want to get the value from mouse event and set it to a state. To add the event listener I used use_node_ref but I am getting the next error:

error[E0597]: `coordinates` does not live long enough
  --> src/test.rs:33:17
   |
24 |           |div_ref| {
   |           --------- value captured here
...
29 |               let listener = Closure::<dyn Fn(MouseEvent)>::wrap(Box::new(|event| {
   |  ________________________________________________________________-
30 | |                 let x = event.x();
31 | |                 let y = event.y();
32 | |                 web_sys::console::log_1(&x.into());
33 | |                 coordinates.set(Coordinates { x: x, y: y });
   | |                 ^^^^^^^^^^^ borrowed value does not live long enough
34 | |                 
35 | |             }));
   | |______________- cast requires that `coordinates` is borrowed for `'static`
...
53 |   }
   |   - `coordinates` dropped here while still borrowed

This is a reproducible example:

use wasm_bindgen::prelude::Closure;
use web_sys::HtmlElement;
use yew::prelude::*;
use wasm_bindgen::JsCast;
use web_sys;
use yew::{function_component, html, use_effect_with_deps, use_node_ref, Html};

struct Coordinates {
    x: i32,
    y: i32,
}

#[function_component]
fn Test() -> Html {
    let div_ref = use_node_ref();
    let coordinates: UseStateHandle<Coordinates> = use_state(|| Coordinates { x: 0, y: 0 });
        

{
    let div_ref = div_ref.clone();
    let coordinates = coordinates.clone();

    use_effect_with_deps(
        |div_ref| {
            let div = div_ref
                .cast::<HtmlElement>()
                .expect("div_ref not attached to div element");

            let listener = Closure::<dyn Fn(MouseEvent)>::wrap(Box::new(|event| {
                let x = event.x();
                let y = event.y();
                web_sys::console::log_1(&x.into());
                coordinates.set(Coordinates { x: x, y: y }); // <- here is the problem
                
            }));

            div.add_event_listener_with_callback(
                "mousemove",
                listener.as_ref().unchecked_ref(),
            )
            .unwrap();

            move || {
                div.remove_event_listener_with_callback(
                    "mousemove",
                    listener.as_ref().unchecked_ref(),
                )
                .unwrap();
            }
        },
        div_ref,
    );
}

    html! {
        <div ref={div_ref}>{"Test"}</div>
    }
}

fn main() {
    yew::Renderer::<Test>::new().render();
}

How can I set the state inside listener in the use_effect_with_deps?


Solution

  • Rust gives you this error because coordinates currently only lives as long as the function it is declared in (that is, once Test ends, coordinates is dropped), but your closures are required to be 'static, which implies outliving the scope of Test (that is, they need to be valid even once Test has ended).

    To avoid this problem, one can move coordinates with the closures, by adding the move keyword before them. This will make the closures take ownership of coordinates, which means it will no longer be dropped at the end of Test.