custom-hook
自作フック 3 つを、それぞれ独自の状態を持つ 2 つのコンポーネントから呼ぶ。
自作フックは、&mut Cx を取ってほかのフックを呼ぶ、普通の関数です。再利用できるようにしているのは #[hook] です。呼び出し位置 を鍵にしたスコープに入るので、2 か所からの呼び出しは ― 同じコンポーネントの中でも、別々のコンポーネントからでも ― それぞれの状態を持ちます。付けなければ、どの呼び出しも1 つのスロットを共有し、同じパスでの 2 回目の呼び出しは衝突として報告されます。機能はこれだけです。登録もトレイトも、ほかに覚えることもありません。自作フックは組み込みのフック そのもの だからです。
ここにある 3 つは use_debounce(値が一定時間変わらなくなったときのその値。1 打鍵ごとにリクエストを投げたくない検索ボックス向け)、use_previous(React のusePrevious を 1 行で)、use_window_size(ウィンドウに反応するレイアウト向け)です。どれもページの中の 2 つのコンポーネントから呼ばれていて、その組は状態を共有しません。片方に入力して、もう片方が動かないのを見てください。
use_debounce は、フックを書く人に付いてくる小さな責任も見せています。変化が落ち着くまでの間、それを落ち着かせるフレームを誰も要求しません。だからフック自身が request_repaint_after でその再描画を頼みます。時間を測るフックは、たいていそうする必要があります。
自分で動かす
sh
cargo run -p custom-hook
trunk serve --config examples/custom-hook/Trunk.tomlソースは examples/custom-hook/src/lib.rs、フック自体は src/hooks.rs にあります。
rust
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> }
}