Skip to content

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> }
}

MIT または Apache-2.0 ライセンスで公開しています。