custom-hook
Three hooks of your own, each called from two components that keep their own state.
A custom hook is an ordinary function that takes &mut Cx and calls other hooks. #[hook] is what makes it reusable: it enters a scope keyed by the call site, so two calls — in one component or in two — get their own state. Leave it off and every call shares one slot, and the second call in a pass is reported as a collision. That is the whole feature; there is no registration, no trait, and nothing else to learn, because a custom hook is the built-in hooks.
The three here are use_debounce (the value as it was once it stopped changing for a delay — a search box that should not fire a request per keystroke), use_previous (React's usePrevious, in one line), and use_window_size (for layout that reacts to the window). Each is called from two different components in the page, and the pairs do not share state — type into one and watch the other stay put.
use_debounce also shows a small responsibility that comes with writing hooks: while a change is still settling, nothing else would ask for the frame that settles it, so the hook asks for that repaint itself with request_repaint_after. A hook that measures time usually has to.
Run it yourself
cargo run -p custom-hook
trunk serve --config examples/custom-hook/Trunk.tomlThe source is examples/custom-hook/src/lib.rs; the hooks themselves are in src/hooks.rs.
use egui_reactor::prelude::*;
use egui_reactor_elements::prelude::*;
pub mod hooks;
use hooks::{use_debounce, use_previous, use_window_size};
/// How long the debounce waits, in seconds.
const DELAY: f64 = 0.5;
/// Below this window width the responsive section stacks instead of spreading.
const NARROW: f32 = 520.0;
#[component]
pub fn App(cx: &mut Cx) {
rsx! {
<View direction="column" gap={8} p={12} grow={1.0}>
<Text size={22.0} strong>"custom hooks"</Text>
<SearchBox/>
<Separator/>
<Mirror/>
<Separator/>
<Counter/>
<Separator/>
<Responsive/>
</View>
}
}
/// `use_debounce` and `use_previous` together: the settled query, and the one
/// before it.
#[component]
fn SearchBox(cx: &mut Cx) {
let mut query = use_state(cx, String::new);
let live = query.clone();
let settled = use_debounce(cx, &live, DELAY);
let previous = use_previous(cx, settled.clone());
rsx! {
<View direction="column" gap={4}>
<Text strong>"search box"</Text>
<TextEdit w={220.0} bind={query.bind()} hint="type to search"/>
<Text>{format!("live: {live}")}</Text>
<Text>{format!("settled: {settled}")}</Text>
<Text>{format!("before that: {}", if previous.as_deref().unwrap_or("").is_empty() {
"(none)"
} else {
previous.as_deref().unwrap_or("(none)")
})}</Text>
</View>
}
}
/// The second caller of `use_debounce`. Its state is its own: type in one box
/// and the other's readings do not move.
#[component]
fn Mirror(cx: &mut Cx) {
let mut text = use_state(cx, || String::from("hello"));
let live = text.clone();
let settled = use_debounce(cx, &live, DELAY);
rsx! {
<View direction="column" gap={4}>
<Text strong>"mirror"</Text>
<TextEdit w={220.0} bind={text.bind()}/>
<Text>{format!("mirror live: {live}")}</Text>
<Text>{format!("mirror settled: {settled}")}</Text>
</View>
}
}
/// The second caller of `use_previous`.
#[component]
fn Counter(cx: &mut Cx) {
let mut count = use_state(cx, || 0i32);
let now = *count;
let before = use_previous(cx, now);
rsx! {
<View direction="row" gap={8} align="center">
<Button on_click={|| *count += 1}>"+"</Button>
<Button on_click={|| *count -= 1}>"-"</Button>
<Text>{format!(
"count {now}, was {}",
before.map_or_else(|| String::from("-"), |n| n.to_string()),
)}</Text>
</View>
}
}
/// `use_window_size` twice: once as a readout, once feeding a layout
/// attribute. A hook can decide how something is laid out, not just what it
/// says.
#[component]
fn Responsive(cx: &mut Cx) {
let size = use_window_size(cx);
let narrow = size.x < NARROW;
rsx! {
<View direction="column" gap={4}>
<Text strong>"responsive"</Text>
<SizeReadout/>
<Text>{format!("layout: {}", if narrow { "column" } else { "row" })}</Text>
<View direction={if narrow { "column" } else { "row" }} gap={8}>
for label in ["one", "two", "three"] {
<View key={label} p={8}>
<Text>{label}</Text>
</View>
}
</View>
</View>
}
}
/// The second caller of `use_window_size`.
#[component]
fn SizeReadout(cx: &mut Cx) {
let size = use_window_size(cx);
rsx! { <Text>{format!("window: {:.0} x {:.0}", size.x, size.y)}</Text> }
}