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

Exposing a way for others to implement their own Hooks #2576

Closed
nicolascotton opened this issue Apr 4, 2022 · 11 comments · Fixed by #2589
Closed

Exposing a way for others to implement their own Hooks #2576

nicolascotton opened this issue Apr 4, 2022 · 11 comments · Fixed by #2589
Labels
A-yew Area: The main yew crate bug feature-request A feature request
Milestone

Comments

@nicolascotton
Copy link

Since all properties/functions of HookContext are either private or pub(crate), it is no longer possible to create custom hooks.

struct CustomHookProvider();

impl Hook for CustomHookProvider {
    type Output = ();

    fn run(self, ctx: &mut HookContext) -> Self::Output {
        (ctx.re_render)(); // re_render is private.
        ctx.next_state(|re_render| {
            // next_state is pub(crate)
        });
        ctx.next_effect(|re_render| {
            // next_effect is pub(crate)
        });
    }
}

In prior versions of Yew (0.19.*), we were able to use use_hook to achieve this goal.
Simply exposing something like the current yew::functional::HookContext::next_state would open the possibility for others to create their own implementations.

@WorldSEnder WorldSEnder added feature-request A feature request A-yew Area: The main yew crate labels Apr 4, 2022
@WorldSEnder
Copy link
Member

WorldSEnder commented Apr 4, 2022

A possible way to get what you want, until further considerations, could be to call the hook functions and immediately run them inline:

struct CustomHookProvider();

impl Hook for CustomHookProvider {
    type Output = ();

    fn run(self, ctx: &mut HookContext) -> Self::Output {
        let reducer_handle = use_reducer(_).run(ctx); // establish state, almost as-if calling `next_state`
        if _ { reducer_handle.dispatch(_); } // Possibly trigger `re_render`
        use_effect(_).run(ctx); // add an effect, almost as-if calling `next_effect`
    }
}

Be wary that for simple cases, a more reasonable implementation of the above is to compose your hooks in a function with #[hook] attached. You get a few more lints and warnings about what not to do:

#[hook]
fn use_custom_hook() {
    let reducer_handle = use_reducer(_); // establish state
    if _ { reducer_handle.dispatch(_); } // Possibly trigger a `rerender`
    use_effect(_); // add an effect
}

@futursolo
Copy link
Member

futursolo commented Apr 4, 2022

You should be using #[hook] to create custom hooks.

Could you please explain what you are trying to do with HookContext?

These functions are not exposed as:

  • next_state is use_reducer and use_state.
  • next_effect is use_effect.
  • re_render is setting anything into a use_state.

but without the compile time checks provided by the procedural macro.
(These functions do not provide any extra features.)

@WorldSEnder
Copy link
Member

WorldSEnder commented Apr 5, 2022

@futursolo the current state of the #[hook] macro has some issues with generic functions. For example I wasn't able to write that by just attaching a #[hook] on top and had to drop down a manual implementation.

@futursolo
Copy link
Member

futursolo commented Apr 5, 2022

@futursolo the current state of the #[hook] macro has some issues with generic functions. For example I wasn't able to write that by just attaching a #[hook] on top and had to drop down a manual implementation.

This is related to impl Trait in return position (associated type type Output = impl Deref<Target = T>;).

We can introduce implicit boxing to fix this, however this can be fixed automatically by type alias impl trait without boxing.
Should we still try to fix it by boxing?

For your particular case, you can expose the concrete type (StyleContainer<T> or Rc<T>), boxing it or register it as a generic parameter.

@nicolascotton
Copy link
Author

nicolascotton commented Apr 5, 2022

In my case I wasn't able to use #[hook], so I had to impl Hook.
The workaround suggested by @WorldSEnder solves my main issue.
The other reason why I wanted a custom Hook was for better optimizations.
The hook I'm trying to make is used in every cells of a large table so I wanted to limit the amount of unnecessary work/allocation made.
One of my concern is the need to create a dummy state to simply trigger a re-render in a stateless hook.

@WorldSEnder
Copy link
Member

WorldSEnder commented Apr 6, 2022

One of my concern is the need to create a dummy state to simply trigger a re-render in a stateless hook.

It might make sense to expose a use_update_signal (name up for debate) hook that exposes re_render for this. I don't think the overhead of the workaround using a ()-state + a dummy reduction function is prohibitively large, but a more specialized version would definitely be more user friendly.

@futursolo
Copy link
Member

futursolo commented Apr 6, 2022

In my case I wasn't able to use #[hook], so I had to impl Hook.

If you are having issues with the #[hook] macro, it might be something we should look into fixing.

Could you please provide a reproducible example?

The other reason why I wanted a custom Hook was for better optimizations.
The hook I'm trying to make is used in every cells of a large table so I wanted to limit the amount of unnecessary work/allocation made.
One of my concern is the need to create a dummy state to simply trigger a re-render in a stateless hook.

If your hook is stateless (only depends on input), you can use the use_memo hook which only updates your output when your input changes.

The built-in hooks are designed to be very efficient, and avoid allocation whenever possible.

The primary cost of a render comes with the rendering process itself instead of the cost of scheduling a render.
The cost of scheduling a render is very minimal.

The runtime cost of scheduling 1 million renders with use_state:

#[function_component]
fn Test() -> 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!(
                "Scheduled 1 million renders in {}ms.",
                instant::now() - start_time
            ));

            || {}
        },
        (),
    );

    Html::default()
}

Result on my device:

Scheduled 1 million renders in 51ms.

Comparing to performing 10 thousand renders:

#[function_component]
fn Test() -> Html {
    let ctr = use_state(|| 0);
    let start_time = use_state(instant::now);

    {
        let ctr_setter = ctr.setter();
        use_effect_with_deps(
            move |ctr| {
                if *ctr < 10_000 {
                    ctr_setter.set(*ctr + 1);
                } else {
                    log!(format!(
                        "Rendered 10 thousand times in {}ms.",
                        instant::now() - *start_time
                    ));
                }

                || {}
            },
            *ctr,
        );
    }

    html! {
        <div>{*ctr}</div>
    }
}

Result on my device:

Rendered 10 thousand times in 42ms.

@nicolascotton
Copy link
Author

nicolascotton commented Apr 7, 2022

The following is an oversimplified version of the case I wasn't able to do with #[hook]:

struct State<T> {
    data: T,
}

impl<T> State<T> {
    fn subscribe(&self, callback: impl Fn(&T, &T)) {
        todo!("Subscription implementation goes here")
    }
    fn unsubscribe(&self) {
        todo!("Unsubscription implementation goes here")
    }
}

#[hook]
fn use_custom_hook<T: 'static, U: 'hook>(
    state: Rc<State<T>>,
    callback: impl Fn(&T) -> &U + 'static,
) -> &'hook U
where
    U: PartialEq,
{
    let renderer = use_state(|| ());
    let value = callback(&state.data);
    use_effect_with_deps(
        {
            let state = state.clone();
            move |_| {
                state.subscribe(move |previous_state, next_state| {
                    // Custom logic to trigger a re-render.
                    let previous_value = callback(previous_state);
                    let next_value = callback(next_state);
                    if previous_value != next_value {
                        renderer.set(()) // trigger a re-render;
                    }
                });
                move || state.unsubscribe()
            }
        },
        (),
    );
    value
}

I'm getting ~2 compilation errors, The first is related to the impl Fn(&T) -> &U + 'static and can be reproduced with the following simplified hook:

// ERROR: cannot provide explicit generic arguments when `impl Trait` is used in argument position
// see issue #83701 <https://github.com/rust-lang/rust/issues/83701> for more information
#[hook]
fn use_impl_fn<T, U>(callback: impl Fn(&T) -> &U) {}

The second is related to a lifetime issue and can be reproduced with the following simplified hook:

// ERROR 1: hidden type for `impl Trait` captures lifetime that does not appear in bounds
// ERROR 2: the type `...` does not fulfill the required lifetime
#[hook]
fn use_external_ref<T, U>(data: T, callback: Box<dyn Fn(&T) -> &U>) -> &'hook U {
    let value = callback(&data);
    use_effect_with_deps(
        move |_| {
            callback(&data);
            || ()
        },
        (),
    );
    value
}

Both issues can be solved if I impl Hook.

@futursolo futursolo added the bug label Apr 7, 2022
@WorldSEnder
Copy link
Member

Can you include a working version of how you solved it with impl Hook, I'm interested. Cause something is going on with lifetimes that doesn't make sense for me in the non-working versions (seems to return references to local variables, which is a no-go in rust anyway).

@futursolo futursolo added this to the v0.20 milestone Apr 7, 2022
@nicolascotton
Copy link
Author

Sure, here it is (using the workaround):

fn use_custom_hook<'hook, T, U>(
    state: &'hook Rc<State<T>>,
    callback: impl Fn(&T) -> &U + 'static,
) -> impl Hook<Output = &'hook U> + 'hook
where
    T: 'static,
    U: PartialEq + 'hook,
{
    struct HookProvider<'hook, T, U, C>
    where
        T: 'static,
        U: PartialEq + 'hook,
        C: Fn(&T) -> &U + 'static,
    {
        state: &'hook Rc<State<T>>,
        callback: C,
    }

    impl<'hook, T, U, C> Hook for HookProvider<'hook, T, U, C>
    where
        T: 'static,
        U: PartialEq,
        C: Fn(&T) -> &U + 'static,
    {
        type Output = &'hook U;

        fn run(self, ctx: &mut yew::HookContext) -> Self::Output {
            let renderer = use_state(|| ()).run(ctx);
            let value = (self.callback)(&self.state.data);
            use_effect_with_deps(
                {
                    let state = self.state.clone();
                    move |_| {
                        state.subscribe(move |previous_state, next_state| {
                            // Logic for re-render
                            let previous_value = (self.callback)(previous_state);
                            let next_value = (self.callback)(next_state);
                            if previous_value != next_value {
                                renderer.set(()) // trigger a re-render;
                            }
                        });
                        move || state.unsubscribe()
                    }
                },
                (),
            )
            .run(ctx);
            value
        }
    }

    HookProvider { state, callback }
}

@futursolo
Copy link
Member

futursolo commented Apr 7, 2022

The first issue is fixed in #2589.

The second issue is not an issue with #[hook].

#[hook]
fn use_external_ref<T, U>(
    data: T, callback: Box<dyn Fn(&T) -> &U> // Provides T, which consumes it.
) -> &'hook U {
    let value = callback(&data); // Borrows T for '_
    use_effect_with_deps(
        move |_| {
            callback(&data); // Trying to move T into a 'static closure after borrow.
            || ()
        },
        (),
    );
    value // Trying to return &U with T being borrowed while T is going to be dropped when the end of function is reached.
}

Solution:

#[hook]
fn use_external_ref<T, U>(data: &Rc<T>, callback: impl 'static + Fn(&T) -> &U) -> &'hook U
where
    T: 'static,
{
    let value = callback(data);

    {
        let data = Rc::clone(data);

        use_effect_with_deps(
            move |_| {
                callback(&data);

                || {}
            },
            (),
        );
    }

    value
}

Although 'hook probably should be applied to &U automatically when a hook returns a reference.

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 bug feature-request A feature request
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants