DioxusLabs / sdk

A library to provide abstractions to access common utilities when developing Dioxus applications.
Apache License 2.0
84 stars 13 forks source link

Add use_window_resize_status hook #47

Closed bcorey closed 4 months ago

bcorey commented 5 months ago

Introduces a wrapper over the use_window_size hook that tells a component whether or not the window has been resized since the last update. This will help devs prevent expensive component size computations from running when not needed.

marc2332 commented 5 months ago

Can you maybe share a bit of context on how you are hitting this expensive computation?

bcorey commented 5 months ago

Hi Marc, thanks for taking a look so quickly. I think when it comes to simple cases where this hook is the only hook that will cause a component update you're right - there's no difference in the number of refreshes. but what if the content of a different hook in the component is changed? I'd like to choose what work to do based on which hook caused the refresh. Let me know if there's a more idiomatic way of doing this, here is additional context:

I am arranging a grid of draggable panels within a DragArea with the grid layout based on window size and I'd like to only run the sizing calculations when the window size changes instead of on every single component update of the parent DragArea. This component experiences many window-size-unrelated updates since itself and all of its direct child DragTargets and Draggables already have to rerender with every pointermove event while dragging. Here is a loose overview of the main logic:

    // when the window is resized:
    // DragArea: get dragArea domrect size via web_sys
    // DragArea: divide dragarea size by desired column count
    // DragArea: divide dragarea size by desired row count
    // DragArea: if resulting row or column size is too small, alter desired row or column counts and repeat
    // 
    // DragArea: send new column and row data to targets (causes refresh of all targets and draggables)
    // DragTarget: targets resize to new data
    // Draggable: docked draggables resize to new data

    // when pointerdown on a draggable:
    // DragArea: start recording pointerpos (refreshes DragArea on every pointermove)
    // Draggable: move active draggable to pointerpos
    // Draggable: each docked draggable checks if pointerpos is within bounds
    //       clears space by moving to active draggable source target if so
    //       if previously moved to clear space, moves to original target if not
    // DragTarget: check if pointer is within rect
    //      set global snap data to rect if so (causes dragarea refresh)

here's what I am envisioning for the DragArea hooks:

#[component]
pub fn DragArea(children: Element) -> Element {
    let global_drag_info = use_context_provider(|| Signal::new(GlobalDragState::new())); // causes refresh on pointermove during a drag
    let drag_area_grid = use_context_provider(|| Signal::new(GridLayout::new()));

    let window_resize_status = use_window_resize_status(); // causes refresh on change
    if let Resized(new_size) = window_resize_status { // don't update all the grid stuff just due to a pointermove while dragging
        drag_area_grid.write().set_new_size(new_size); // will cause an additional refresh of this and all children with the new grid data
    }
    rsx!{...}
 }
DogeDark commented 5 months ago

I would suggest looking at use_effect or use_memo. They only re-run when the signals used in them change. e.g.

#[component]
pub fn DragArea(children: Element) -> Element {
    let global_drag_info = ...;
    let drag_area_gird = ...;

    let window_size = use_window_size();
    use_effect(move || {
        // This subscribes this effect to the signal returned by `use_window_size`
        // and will only re-run when it's value changes.
        let size = window_size();

        // Do your computational code
        drag_area_grid.write().set_new_size(size);
    });

    rsx!{...}
}

See use_effect & use_memo

bcorey commented 4 months ago

Thanks for pointing out these two hooks - they look promising but I'm still encountering an issue this PR solves: signals written to from inside the use_memo or use_hook closure seem to be cloned or do not update the signal value stored in the component (I have already checked and the code is indeed running when expected, just not storing the new values in the signal). Is there an easy way around this? Here are how the two approaches look for me in the current iteration:

#[component]
fn Draggable(...) -> Element {
    let id = use_signal(|| uuid::Uuid::new_v4().to_string());
    let mut local_drag_info =
        use_context_provider(|| Signal::new(LocalDragState::new(variant, id())));
    let global_drag_info = use_context::<Signal<GlobalDragState>>();

    // let window_size = use_window_size();
    // use_memo(move || {
    //     let _ctx = window_size();
    //     // local_drag_info is undesirably cloned/does not mutate the signal stored by the component
    //     local_drag_info.write().resize_snapped();
    // });

    let window_size_info = use_window_resize_status();
    // local_drag_info mutates the signal stored by the component as desired
    if let WindowSizeWithStatus::Resized(_new_size) = window_size_info {
        local_drag_info.write().resize_snapped();
    }

    [...]

    rsx! {...}
}
DogeDark commented 4 months ago

It is normal for signals themselves to be cloned but the value stored inside may or may not be depending on how it's used. In your case, the inner value shouldn't be cloned at all since .write() returns a mutable reference. As for the signal not updating, what method are you using to check it?

bcorey commented 4 months ago

I've investigated further and filed DioxusLabs/dioxus/#2565 to account for the issue I'm having with use_memo

bcorey commented 4 months ago

closing now that I have the use_memo approach working. thanks for the help!