Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce a dedicated use_force_update hook #2586

Merged
merged 2 commits into from
Apr 9, 2022

Conversation

WorldSEnder
Copy link
Member

@WorldSEnder WorldSEnder commented Apr 7, 2022

Description

Introduce a hook that can unconditionally trigger a rerender. Avoids an idiosyncratic use of use_reducer or use_state and introduces a few less allocations.

Targets, but doesn't complete #2576 (closes the last api gap)

Checklist

  • I have run cargo make pr-flow
  • I have reviewed my own code
  • I have added tests

@WorldSEnder WorldSEnder changed the title introduce a dedicated use_force_update hook Introduce a dedicated use_force_update hook Apr 7, 2022
github-actions[bot]
github-actions bot previously approved these changes Apr 7, 2022
@github-actions
Copy link

github-actions bot commented Apr 7, 2022

Visit the preview URL for this PR (updated for commit a8043fd):

https://yew-rs--pr2586-use-rerender-icb12nwz.web.app

(expires Thu, 14 Apr 2022 16:16:25 GMT)

🔥 via Firebase Hosting GitHub Action 🌎

@github-actions
Copy link

github-actions bot commented Apr 7, 2022

Size Comparison

examples master (KB) pull request (KB) diff
boids 315.773 315.773 0
contexts 236.062 236.050 -0.012
counter 169.850 169.854 +0.004
dyn_create_destroy_apps 177.897 177.886 -0.012
file_upload 199.303 199.307 +0.004
function_memory_game 353.684 353.688 +0.004
function_router 405.566 405.571 +0.005
function_todomvc 328.789 328.777 -0.012
futures 366.854 366.846 -0.008
game_of_life 211.813 211.817 +0.004
inner_html 161.010 161.014 +0.004
js_callback 176.896 176.884 -0.012
keyed_list 333.447 333.436 -0.012
mount_point 168.754 168.758 +0.004
nested_list 229.526 229.515 -0.012
node_refs 175.394 175.397 +0.004
password_strength 1859.103 1859.103 0
portals 188.427 188.431 +0.004
router 589.927 589.935 +0.008
simple_ssr 572.967 572.967 0
ssr_router 495.048 495.048 0
suspense 225.436 225.424 -0.012
timer 175.672 175.660 -0.012
todomvc 274.601 274.604 +0.004
two_apps 170.720 170.708 -0.012
webgl 175.251 175.255 +0.004

@WorldSEnder
Copy link
Member Author

WorldSEnder commented Apr 7, 2022

This hook is consistently faster (x 6-7) on my Laptop than the alternative of using state.

#[function_component]
fn BenchSetState() -> Html {
    let state = use_state(|| ());

    use_effect_with_deps(
        move |_| {
            let start_time = instant::now();

            for _ in 0..1_000_000 {
                state.set(());
            }

            log!(format!(
                "BenchSetState: Scheduled 1 million renders in {}ms.",
                instant::now() - start_time
            ));

            || {}
        },
        (),
    );

    Html::default()
}

#[function_component]
fn BenchForceUpdate() -> Html {
    let state = use_force_update();

    use_effect_with_deps(
        move |_| {
            let start_time = instant::now();

            for _ in 0..1_000_000 {
                state.force_update();
            }

            log!(format!(
                "BenchForceUpdate: Scheduled 1 million renders in {}ms.",
                instant::now() - start_time
            ));

            || {}
        },
        (),
    );

    Html::default()
}

Firefox:

BenchForceUpdate: Scheduled 1 million renders in 50ms.
BenchSetState: Scheduled 1 million renders in 344ms.

Chrome:

BenchForceUpdate: Scheduled 1 million renders in 20.900000005960464ms.
BenchSetState: Scheduled 1 million renders in 133.90000000596046ms.

Copy link
Member

@futursolo futursolo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this pull request resolves the bigger question raised in the issue (why they cannot use #[hook] to create a hook). Which is an issue that I think should be investigated before v0.20.

In addition, even with the performance improvement, I don't think it's good to have a hook to encourage rendering without changing any state.

If a function component's rendered layout is purely determined by props + context + states, all of them will automatically subscribe for a re-render upon changes.

This is true even if Yewdux or Bounce is used, which an Rc'ed copy of the state will be produced and set into a Yew-managed state.

If you have a state not managed by Yew (or interior mutability), there's no guarantee that your state will stay the same until the time a render actually happens (as render is delayed), which results in inconsistency in rendered result.

This situation will only get worse if more concurrent mode features are introduced.

@WorldSEnder
Copy link
Member Author

WorldSEnder commented Apr 7, 2022

If a function component's rendered layout is purely determined by props + context + states, all of them will automatically subscribe for a re-render upon changes.

Which is true as long as you don't use internally mutable state or state that is not managed by Yew. In such a situation, rather than introducing fake state () that you "set", there's this dedicated hook that just does what you want to do.

I can't fathom how many useless clones of data is made under the immutable model that react pushes to protect developers from themselves ("never use mutable structure", "use spread syntax", etc). And to some degree the solution is libraries offering cheaply cloneable, immutable, i.e. persistent data structures in rust (I see the PR for that). But you won't be able to retrofit libraries to that model, and to really push performance, at some point mutable data just is faster. (I think the theoretical bound was some kind of log factor for functional vs algorithms on mutable data but I can't recall the specifics).

If you have a state not managed by Yew (or interior mutability), there's no guarantee that your state will stay the same until the time a render actually happens (as render is delayed), which results in inconsistency in rendered result.

Consistency is even more of a problem if I start cloning the state all over instead of relying on a single-source of truth. Matter of fact, as long as the view function fetches consistent state, I don't see what could go wrong, even if the render is delayed. Any component should be written to expect a re-render at any point, after all. To say it another way: The consistency of the state-management should never depend on whether or not some component interested in part of it is (re)rendering or not.

It's a fundamental part of the model that at some point a re-render is triggered. Just embrace it and offer a hook for it, with a few warning labels attached. The standard rust programmer is different from the standard javascript programmer and exposing some more low level primitives shouldn't hurt. Maybe I would add a warning that the re-render isn't guaranteed to always run immediately? In any case, I don't quite see how concurrent mode will mess with something as fundamental as triggering a re-render.

In addition, even with the performance improvement, I don't think it's good to have a hook to encourage rendering without changing any state.

I also don't want to encourage it, that's why the warnings are there and all the docs tell you to prefer something else. But I want it to be there if someone really needs it without sacrificing performance. Maybe keep it out of the website or move it somewhere under the "Advanced topics" section.

@WorldSEnder
Copy link
Member Author

WorldSEnder commented Apr 7, 2022

Btw, the "Facebook React" way (update: that wasn't it, new experiment) of both explaining and 'solving' the problem. I'm sure that API is much easier to reason about. NOT.

@ranile
Copy link
Member

ranile commented Apr 7, 2022

One place I could see this hook being very useful is if I want to have a context without wrapping it in a reducer. For example:

struct Context {
    data: RefCell<String> // or something else, this type doesn't matter,
    re_render: UseForceUpdate
}

impl Context {
    fn data(&self) -> &String { &*self.data.borrow() }

    fn set_data(&self, value: String) { 
        *self.data.borrow_mut() = value
        self.re_render.force_update()
    }
}

This is a lot less boilerplate-y compared to a solution which uses a reducer. This goes together with my comment #2563 (comment):

I can't help but feel like we are going in the opposite direction of what Rust tells us to do

Rust offers us mutability (interior mutability is part of it) and we should embrace it instead of trying to bend over to offer a JS-way to solve a problem

@futursolo
Copy link
Member

futursolo commented Apr 7, 2022

Any component should be written to expect a re-render at any point, after all.

How do you do it without securing it with expected state that is guaranteed to show as expected after the next time its written to?

Consistency is even more of a problem if I start cloning the state all over instead of relying on a single-source of truth.

No, The answer is in the link you posted and the update version: reactwg/react-18#86

The standard rust programmer is different from the standard javascript programmer
I also don't want to encourage it,

Rust has a nature to make discouraged practice "difficult to use" with .unwrap(), unsafe { }, etc... (that you feel this is "wrong" and there must be better ways to do it), why should we do the contrary and make it easier?

@WorldSEnder
Copy link
Member Author

WorldSEnder commented Apr 7, 2022

Rust has a nature to make discouraged practice "difficult to use" with .unwrap(), unsafe { }, etc..., why should we do the contrary and make it easier?

I don't see how any of those are "difficult" to use, seems pretty straight forward.

Rust offers us mutability (interior mutability is part of it) and we should embrace it instead of trying to bend over to offer a JS-way to solve a problem

Agreed.

No, The answer is in the link you posted and the update version: reactwg/react-18#86

Ah sure, seems a bit easier to use. And totally increases my trust in their design process.

But my main concern with this PR is not to allow reading from mutable source, but just offer a bail-out mechanism. This is not concerned with Concurrent Mode or Tearing (which is more-or-less a problem with not being able to identify rendering context without the additional API), which should be solved when it comes up. This only triggers an unconditional rerender, for any reason and I expect it to be safe, although maybe causing slightly too many re-renders once CM exists and we can think about more high-level API for that. But semantically, calling force_update only schedules another render, for an unspecified reason, at some point in the future, and it trusts the user to know what they're doing with that (something that I miss in insert popular framework here).

Like I asked before: would it be reasonable to move the website documentation to give the hook less exposure to beginners?

@futursolo
Copy link
Member

But my main concern with this PR is not to allow reading from mutable source

The main reason you want to do this is you are trying to read from states not synchronised with yew's rendering cycle.

FWIW, yew-hooks already has a hook that does this.

https://docs.rs/yew-hooks/latest/yew_hooks/fn.use_update.html

If this is discouraged, and you need to schedule many many renders (very unoptimised) before a render happens to make any realistic difference, is there any good reason to make it available in yew?

@WorldSEnder
Copy link
Member Author

is there any good reason to make it available in yew

Yes, the fact that if it's used, at least it's reasonably efficient. And people will (and apparently already have) hack their way around it if it doesn't exist.

@ranile
Copy link
Member

ranile commented Apr 8, 2022

The fact that Yew is a Rust library makes things different. Rust is about giving control. If we can give control, we should. However we should also have general API that are to be used in vast majority of cases

Copy link
Member

@ranile ranile left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can go ahead and merge this

Comment on lines +25 to +30
// #![feature(fn_traits)] // required nightly feature to make UseForceUpdate callable directly
// impl Fn<()> for UseForceUpdate {
// extern "rust-call" fn call(&self, _args: ()) {
// self.force_update()
// }
// }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should put this behind a nightly feature flag but that can be done in a separate PR

/// A handle which can be used to force a re-render of the associated
/// function component.
#[derive(Clone)]
pub struct UseForceUpdate {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we follow UseXXXHandle convention and rename it to UseForceUpdateHandle? like UseReducerHandle / UseStateHandle etc

@jetli
Copy link
Contributor

jetli commented Apr 9, 2022

From yew-hooks aspect, it uses use_update a lot to do interior mutability, e.g. use_list / use_map / use_set / use_queue, then we can avoid clone large collections every time, and we're quite sure to use it correctly and states are synced in these scenarios. If use_force_update is much faster than use_update, I will happily to use it and deprecate my use_update. And the docs gives good caution info to discourage to use it, so I might vote for a merge.

@futursolo
Copy link
Member

futursolo commented Apr 9, 2022

interior mutability

I originally planned to discuss this topic when I have time to implement use_transition. But I guess I should explain a little bit what concurrent mode does and why it does not work well with interior mutability.

There is an important distinction between:

  1. fast (but blocking)
  2. keeping browser responsive (but slower until user sees the update)

Blocking means that an update must happen on the spot, regardless of whether the browser is busy or not.
And a non-blocking update is an update that can be run at a time when the browser is not busy (requestIdleCallback).

Interior mutability means choosing the first option, which seems fast, but at the cost of blocking the browser until everything is settled.

image

Concurrent mode, on the other hand, elects the second option, for updates that can be delayed, it updates in a copy of the state when the browser is idle. This means that multiple copies of a state will exist "concurrently".

image

However, this means that if in the meanwhile an urgent update happens, it will "rollback" instead of "commit".

image

Interior Mutability means we cannot "rollback" when a transition becomes outdated before committing.
(State updates during transition is just one of the circumstances where the transition needs rollback.)

Extensively using interior mutability means that your state always need to be committed in "urgent" mode so that later updates will be able to see previous updates, which blocks the browser.

I think it may be fine to merge this at the moment, but we might need to revisit the design of this hook in the future (by making it into a hook that marks the current render as "urgent"?).

@ranile
Copy link
Member

ranile commented Apr 9, 2022

We don't have concurrent mode yet (to be honest, I don't even understand what it is and what problem it solves). If we plan on landing concurrent mode before 1.0, we can make a breaking change if needed. I'll merge this now as it (and any other APIs) can be updated before a stable release as needed.

@futursolo it would be nice if you could create a discussion/issue about concurrent mode, explaining what it is, why it's needed and what problem it solves. That would help those (myself included) who haven't used it in other ecosystems, such React.

@ranile ranile merged commit 6992a45 into yewstack:master Apr 9, 2022
@WorldSEnder WorldSEnder deleted the use_rerender branch April 9, 2022 13:21
@ranile ranile mentioned this pull request Jun 20, 2022
2 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-yew Area: The main yew crate documentation ergonomics
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants