Open rebo opened 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)
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.
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?
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)
],
]
}
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?
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!
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:"],
]
}
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())]
}
after_render_once(|frame_data| ..
- struct AnimationFrameData
with methods/props timestamp
and delta
.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:"],
]
}
dependencies
- I think it was the main problem why we (with tatrix) weren't able to understand it.StateChangedTrait
- you can save the previous value and just compare the previous and the current one (if it makes sense in your use-case) and it should be a little bit more explicit (.changed()
).after_render_once
& after_render_if
or after_render_once
& after_render
.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.
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.
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.
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 .
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())]
}
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.