44 releases

3.0.0-alpha.15 Aug 21, 2024
3.0.0-alpha.13 Jul 29, 2024
3.0.0-alpha.10 Jun 19, 2024
3.0.0-alpha.3 Feb 20, 2024
1.0.0-alpha.18 Nov 23, 2022

#230 in Programming languages


Used in hooks-yew

MIT license

245KB
6K SLoC

::hooks

Crates.io docs.rs GitHub license GitHub stars

Compile-time, async hooks in safe Rust.

Quick Start

Run cargo add hooks to add hooks to your project.

Note that this project is still in alpha, and it may have BREAKING CHANGES.

Please see changelogs before upgrading.

You can compose hooks in a hook fn with hook_fn!(...) or #[hook]

With hook_fn!(...)

use hooks::prelude::*;

hook_fn!(
    fn use_demo() {
        let (state, updater) = h![hooks::use_shared_set(0)];

        let updater = updater.clone();

        h![hooks::use_effect(move |v: &i32| {
            println!("state = {}", *v);

            if *v < 2 {
              updater.set(*v + 1);
            }
        }, *state)];
    }
);

futures_lite::future::block_on(async {
    let mut hook = use_demo().into_hook();
    while let Some(()) = hook.next_value().await {}
});

With #[hook]

This attribute macro is only available under proc-macro feature. Enable it with cargo add -p hook --features proc-macro. #[hook] allows using hooks without h!(). Any function call or method call with fn name starting with use_ is automatically detected as a hook.

use hooks::prelude::*;

#[hook]
fn use_demo() {
    let (state, updater) = hooks::use_shared_set(0);

    let updater = updater.clone();

    hooks::use_effect(move |v: &i32| {
        println!("state = {}", *v);

        if *v < 2 {
          updater.set(*v + 1);
        }
    }, *state);
}

fn main() {
    futures_lite::future::block_on(async {
        let mut hook = use_demo().into_hook();
        while let Some(()) = hook.next_value().await {}
    });
}

You will see the following logs. Then the program exits gracefully because it knows there won't be new values.

state = 0
state = 1
state = 2

What is a compile-time Hook?

To understand the concepts behind this crate, you can expand this section and read the details.

Hooks, introduced by React 16.8, is a way to bring state into functional components. Hooks can make stateless functional components stateful, and reactive.

Conventional hook implementations use a global state to record hook calls and their states. This way, a stateless function can maintain its state through runtime contexts. Thus, the order of hook calls must not change; conditional hook calls are also forbidden. Developers must follow Rules of Hooks to write a valid custom hook. yew.rs also passes hook contexts to used hooks. We can see the above implementation relies on runtime behavior of a hook fn. The hook runner must run the function once to know what is initialized. We call this runtime hooks.

Rust language has powerful static type systems. In fact, the state of a hook function is statically typed. The hard problem is to make the stateless function stateful, which means its state should also be known by the executor. We call this kind of hook implementation as compile-time hooks.

This crate defines and implements compile-time hooks for you.

When a type implements Hook, it defines three behaviors:

  1. When using this hook, what does it output?

    Hook::use_hook returns HookValue::Value.

    This crate uses GAT (Generic Associated Types) to allow the output type borrowing from the hook itself. Due to some limitations of real GAT, this crate uses better GAT pattern introduced by Sabrina Jewson. Thanks to her!

  2. When should we re-use this hook?

    Hooks have states. When the state doesn't change, we don't need to re-call use_hook to get the new output. We can wait for the hook's state to change with HookPollNextUpdate::poll_next_update, or by just hook.next_update().await.

    To wait for the next value when state changes, you can use hook.next_value().await method.

How to write a custom hook?

Please see Hook trait.

How to use hooks in a hook function?

With hook_fn! macro, you can just use h![use_another_hook(arg0, arg1)] at top level token trees (not wrapped in token trees like (), [], or {}). The macro will transform the call.

With #[hook] macro, you can just call use_another_hook(arg0, arg1) at top level expressions (not in an inner block like {}). The macro will transform the call. You can see the snapshots for what this macro outputs.

How to conditionally use hooks?

Please see use_lazy_pinned_hook and use_uninitialized_hook.

How to use the hook when not in a hook fn

A hook fn actually returns impl UpdateHookUninitialized. To consume it, you can run use_my_hook().into_hook() to turn it into a Hook, or run use_my_hook().into_hook_values() (which runs use_my_hook().into_hook().into_values()) to get async iterated values.

To consume a Hook, you can use its next value with hook.next_value().await. You can get async iterated values with hook.values() or hook.into_values(), which is a Stream if the hook is NonLendingHook.

# use hooks::prelude::*;
hook_fn!(
    fn use_demo() -> i32 {
        let (state, updater) = h![use_shared_call(
            0,
            |v| {
                if *v < 2 {
                    *v += 1;
                    true // indicating state is updated
                } else {
                    false // indicating state is not updated
                }
            },
        )];

        let updater = updater.clone();
        h![hooks::use_effect(move |_: &i32| {
            updater.call();
        }, *state)];

        *state
    }
);

// with hook.next_value().await
futures_lite::future::block_on(async {
    let mut hook = use_demo().into_hook();
    assert_eq!(hook.next_value().await, Some(0));
    assert_eq!(hook.next_value().await, Some(1));
    assert_eq!(hook.next_value().await, Some(2));
    assert_eq!(hook.next_value().await, None);
});

// with hook.into_hook_values() and stream.next().await
futures_lite::future::block_on(async {
    use futures_lite::StreamExt;

    let mut values = use_demo().into_hook_values();
    assert_eq!(values.next().await, Some(0));
    assert_eq!(values.next().await, Some(1));
    assert_eq!(values.next().await, Some(2));
    assert_eq!(values.next().await, None);
});

// with hook.into_hook_values() and stream.collect().await
futures_lite::future::block_on(async {
    use futures_lite::StreamExt;

    let values = use_demo().into_hook_values();
    let values = values.collect::<Vec<_>>().await;
    assert_eq!(values, [0, 1, 2]);
});

Dependencies

~115–300KB