rebo / seed-quickstart-hooks

Example of how react style hooks might integrate with Seed.
5 stars 0 forks source link

after_render() aka useEffect #7

Open rebo opened 4 years ago

rebo commented 4 years ago

Have implemented React's useEffect using the after_render() function, this function accepts a bool to force running and a closure which will run after the dom has been rendered.

React calls this functionality useEffect because it allows for side-effects outside of the usual react render pipeline. Some things that useEffect/after_render are used for include manipulating the dom once a component has been rendered or setting a timer/interval once dom elements have been rendrered.

The after_render closure will only ever be run once.

fn after_example() -> Node<Msg> {
    after_render(false, || {
        document().set_title("The Page has been rendered");
        if let Some(my_div) = get_html_element_by_id("my_div") {
            my_div.set_inner_text("This div has been rendered");
        }
    });
    div![id!("my_div"), "Not Rendered"]
}
MartinKavik commented 4 years ago

My first glance on the mobile phone - closure should have (?) one parameter: timestamp to sync animation (like orders.after_render). I probably don't understand bool variable usage - does true mean Seed rerenders dom on every animation frame and invokes the closure after each render? (and false = run closure only when the parent view function has been invoked)

rebo commented 4 years ago

The afterrender could have a timestamp parameter, currently it is not passed through to mimic React's useEffect but adding it back in is trivial. The only disadvantage would be that one would use `` when the timestamp isn't needed so i'll just add it.

When the argument is false then the closure will run a single time after the next render. This would be the normal usage.

When the argument is true then the closure will run no matter what (after the next render). This could be needed sometimes to force the closure to run again based on some setting in the &Model.

TatriX commented 4 years ago

When the argument is true then the closure will run no matter what (after the next render). This could be needed sometimes to force the closure to run again based on some setting in the &Model.

Can you show an example of when that is needed?

rebo commented 4 years ago

Can you show an example of when that is needed?

Sure you may want to recalculate the current rendered width of a div programatically for example for placement of a popover. The below calculates this width on first render, but then also recalculates it everytime the button is clicked.

fn other_examples() -> Node<Msg> {
    let recalculate_width = use_state(|| false);

    after_render(recalculate_width.get(), move || {
        if let Some(my_div) = get_html_element_by_id("my_div2") {
            if let Ok(Some(style)) = window().get_computed_style(&my_div) {
                my_div.set_inner_text(&format!(
                    "width of this div = {}",
                    style.get_property_value("width").unwrap()
                ))
            }
        }
        recalculate_width.set(false);
    });

    div![
        div![id!("my_div2")],
        button![
            "Calculate Width of div",
            recalculate_width.mouse_ev(Ev::Click, |recalc, _| *recalc = !*recalc)
        ],
    ]
}
TatriX commented 4 years ago

Sorry, I still don't get it. So you call after_render(false, ... in the example, right? Also I think I haven't seen StateAccess::mouse_ev before. Is it equivalent to

mouse_ev(Ev::Click, move |_| recalculate_width.update(|recalc| *recalc = !*recalc))

?

When the argument is false then the closure will run a single time after the next render. When the argument is true then the closure will run no matter what (after the next render).

So this means that sometimes closure won't run after next render? This is a bit confusing, maybe instead of bool we should use either 2 different functions or use descriptive enum?

MartinKavik commented 4 years ago

I agree with @TatriX - I don't have an idea how it works. Could you write some simple "real-world" examples where you want to use false and where you want to use true? Thanks!

rebo commented 4 years ago

Ok been thinking about this a bit. These two articles give a bit more of a thorough breakdown of useEffect and the point of the various subtleties of it:

The bool flag that was in after_render was a stand in for the dependency array mentioned above. The short version is that the "effect" would only run if a variable in the dependency array had changed since the last time the closure ran. The point of this would be to trigger an effect if dependencies were updated.

An example might be triggering a fetch, executing some javascript (say a mathjax render), or logging some information to a dom element outside of the main app if a watched variable changed.

In view of this I therefore have working the following three functions:

pub fn after_render_once<F: Fn() -> () + 'static>(func: F) 

in the above function the closure runs once and once only after the component is rendered.

pub fn after_render_deps<F: Fn() -> () + 'static>( dependencies: &[impl StateChangedTrait], func: F,) 

in the above function the closure runs only if any of the dependencies changed.

pub fn after_render_always<F: Fn() -> () + 'static>(func: F) 

in the above function the closure runs every time the page is rendered (probably rarely used).

Here is some example code of the above. This one demonstrates setting the focus of an input element on first page render. As you can see the code is very clean.

fn focus_example() -> Node<Msg> {
    let input_string = use_state(String::new);

    after_render_once(move || {
        if let Some(elem) = get_html_element_by_id(&input_string.identity()) {
            let _ = elem.focus();
        }
    });

    input![id!(input_string.identity())]
}

In this second example we log the smallest of two inputs directly to a ul element. The closure only runs if input_a or input_b had changed.

fn deps_example() -> Node<Msg> {
    use std::cmp::Ordering;
    let input_a = use_istate(String::new);
    let input_b = use_istate(String::new);

    after_render_deps(&[input_a, input_b], move || {
        if let (Ok(a), Ok(b)) = (input_a.get().parse::<i32>(), input_b.get().parse::<i32>()) {
            let smallest = match a.cmp(&b) {
                Ordering::Less => "<li>A is the smallest</li>",
                Ordering::Greater => "<li>B is the smallest</li>",
                Ordering::Equal => "<li>Neither is the smallest</li>",
            };

            if let Some(elem) = get_html_element_by_id("list") {
                let _ = elem.insert_adjacent_html("beforeend", smallest);
            }
        }
    });

    div![
        "A:",input![bind(At::Value, input_a)],
        "B:",input![bind(At::Value, input_b)],
        ul![id!("list"), "Smallest Log:"],
    ]
}
MartinKavik commented 4 years ago

I think I understand now, thanks. I'll try to modify your API so we have more drafts to compare. A) Focus input

fn focus_example() -> Node<Msg> {
    let input = use_state(ElRef::default);
    after_render_once(move |_| input.get().get().expect("input element").focus().expect("focus input"));
    input![el_ref(&input.get())]
}

B) Compare inputs

fn compare_example() -> Node<Msg> {
    use std::cmp::Ordering;
    let input_a = use_state(String::new);
    let input_b = use_state(String::new);
    let list = use_state(ElRef::default);

    after_render_if(input_a.changed() || input_a.changed(), move |_| {
        if let (Ok(a), Ok(b)) = (input_a.get().parse::<i32>(), input_b.get().parse::<i32>()) {
            let smallest = match a.cmp(&b) {
                Ordering::Less => "<li>A is the smallest</li>",
                Ordering::Greater => "<li>B is the smallest</li>",
                Ordering::Equal => "<li>Neither is the smallest</li>",
            };
            list
               .get()
               .get().expect("list element")
               .insert_adjacent_html("beforeend", smallest).expect("insert html")
        }
    });

    // --- OR (is it possible?)---
    if input_a.changed() || input_a.changed() {
        after_render(move |_| {       // after_render ~= after_render_always
           // ... 
       } 
    }
   // --- // ---

    div![
        "A:", input![bind(At::Value, input_a)],
        "B:", input![bind(At::Value, input_b)],
        ul![el_ref(&list.get()), "Smallest Log:"],
    ]
}

rebo commented 4 years ago

Hi yes the above looks good. The changed() example was how I used to do it with the bool. as you mentioned this makes the usage flexible without further trait constraints needed.

after_render_once looks good.

The question is do we want after_render_if with the first argument being a bool or probably more simply just after_render which always fires which can be behind a normal rust if conditional I.e.

if input_a.changed() || input_b.changed {
    after_render(|_| ...);
}

I think the main reason react combines the design conditional into the use effect hook is because they have limitations on placement of hooks in a function bodies which we dont have.

MartinKavik commented 4 years ago

I think the main reason react combines the design conditional into the use effect hook is because they have limitations on placement of hooks in a function bodies which we dont have.

If we don't have those limitations => it means that the answer to my question // --- OR (is it possible?)--- is yes => then after_render_once & after_render is a clear winner here I think.

MartinKavik commented 4 years ago

Or we can make "once handling" general and introduce a helper method run_once. Then after_render is enough and we can use it in if block or with run_once/once! - e.g. run_once(|| after_render... / once!{ after_render... }. Just idea.

rebo commented 4 years ago

Ah yes do_once (synchronous) is already implemented in comp_state so we can just use that :).

On Mon, 17 Feb 2020, 17:38 Martin Kavík, notifications@github.com wrote:

Or we can make "once handling" general and introduce a helper method run_once. Then after_render is enough and we can use it in if block or with run_once - e.g. run_once(|| after_render.... Just idea.

— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub https://github.com/rebo/seed-quickstart-hooks/issues/7?email_source=notifications&email_token=AAAD6VQO4DXMMEZ6XRWYGQ3RDLDS3A5CNFSM4KSAL6RKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEL7GRKQ#issuecomment-587098282, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAAD6VUWZXT6JLXSK3DVURDRDLDS3ANCNFSM4KSAL6RA .

rebo commented 4 years ago

Ok here is the final(?) api.

run an after_render effect once by calling inside a do_once block:

fn focus_example() -> Node<Msg> {
    let input = use_state(ElRef::default);

    do_once(|| {
        after_render(move |_| {
            let input_elem: web_sys::HtmlElement = input.get().get().expect("input element");
            input_elem.focus().expect("focus input");
        });
    });
    input![el_ref(&input.get())]
}

conditional depending on changed states:

fn if_example() -> Node<Msg> {
    use std::cmp::Ordering;
    let input_a = use_istate(String::new);
    let input_b = use_istate(String::new);

    if input_a.changed() || input_b.changed() {
        after_render(move |_| {
            if let (Ok(a), Ok(b)) = (input_a.get().parse::<i32>(), input_b.get().parse::<i32>()) {
                let smallest = match a.cmp(&b) {
                    Ordering::Less => "<li>A is the smallest</li>",
                    Ordering::Greater => "<li>B is the smallest</li>",
                    Ordering::Equal => "<li>Neither is the smallest</li>",
                };

                if let Some(elem) = get_html_element_by_id("list") {
                    let _ = elem.insert_adjacent_html("beforeend", smallest);
                }
            }
        });
    }

    div![
        "A:",
        input![bind(At::Value, input_a)],
        "B:",
        input![bind(At::Value, input_b)],
        ul![id!("list"), "Smallest Log:"],
    ]
}

as a convenience after_render_once is still implemented (it is just synactic sugar for the do_once(|| after_render( || ...pattern:

fn focus_example() -> Node<Msg> {
    let input = use_state(ElRef::default);

    after_render_once(move |_| {
        let input_elem: web_sys::HtmlElement = input.get().get().expect("input element");
        input_elem.focus().expect("focus input");
    });

    input![el_ref(&input.get())]
}