Crates.io | hooks |
lib.rs | hooks |
version | 3.0.0-alpha.15 |
source | src |
created_at | 2022-09-30 15:33:38.116144 |
updated_at | 2024-08-21 16:14:26.087132 |
description | Compile-time, async hooks |
homepage | |
repository | https://github.com/frender-rs/hooks |
max_upload_size | |
id | 677383 |
size | 193,405 |
::hooks
Compile-time, async hooks in safe Rust.
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]
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 {}
});
#[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
Hook
?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:
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!
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.
Please see Hook
trait.
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.
Please see [use_lazy_pinned_hook
] and [use_uninitialized_hook
].
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]);
});