1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Dioxusのhooks入門 増補版

Posted at

Dioxusの7つのフックの目的、使い方をまとめる

Dioxusのフックは、Reactのフックと同様に、コンポーネントの状態管理やライフサイクルイベントを扱うための機能。以下に、よく使われるフックを目的とサンプルコードによる使い方を紹介していく(Geminiちゃん、ありがとう)。

dioxusのインストールとサンプルプログラムの実行方法:

git clone https://github.com/DioxusLabs/dioxus.git
cd dioxus/examples
cargo run --example xxxx  # これでxxxx.rsが実行される

1. use_signal

最も頻繁に使われるフック。コンポーネントの状態を管理し、その状態が変更されたときに自動的にUIを再レンダリングする。これは、ReactのuseStateに相当する機能だが、より直感的に使える。
目的: コンポーネントの状態を宣言し、UIをリアクティブに更新する。

例: readme.rs ( 動作確認は、cargo run --example readme )
この例では、use_signalを使ってcountという状態を管理し、ボタンがクリックされるたびにcountを更新して再レンダリングする。

    let mut count = use_signal(|| 0);

    rsx! {
        h1 { "High-Five counter: {count}" }
        button { onclick: move |_| count += 1, "Up high!" }
        button { onclick: move |_| count -= 1, "Down low!" }
    }

2. use_effect

コンポーネントのライフサイクルイベント(コンポーネントのレンダリング後など)で副作用(データの取得、DOM操作など)を実行するために使われる。ReactのuseEffectに相当する。
目的: コンポーネントが初めてレンダリングされたときや、特定の状態が変更されたときに、特定のコードを実行する。

例: future.rs
この例では、use_futureと対比させて説明している

fn app() -> Element {
    let mut count = use_signal(|| 0);

    // use_future is a non-reactive hook that simply runs a future in the background.
    // You can use the UseFuture handle to pause, resume, restart, or cancel the future.
    use_future(move || async move {
        loop {
            sleep(std::time::Duration::from_millis(200)).await;
            count += 1;
        }
    });

    // use_effect is a reactive hook that runs a future when signals captured by its reactive context
    // are modified. This is similar to use_effect in React and is useful for running side effects
    // that depend on the state of your component.
    //
    // Generally, we recommend performing async work in event as a reaction to a user event.
    use_effect(move || {
        spawn(async move {
            sleep(std::time::Duration::from_secs(5)).await;
            count.set(100);
        });
    });

use_effect:
リアクティブ: 初回描画時に実行され、さらにフック内で使用されているシグナルが変更されるたびに再描画される。
用途: 特定の状態(シグナル)の変化をトリガーとして副作用(Side Effect)を実行したい場合に適している。例えば、「userIdが変わったら、新しいユーザーデータを取得する」といった処理。
上記の例では、描画開始5秒後に値が100になる

use_future:
非リアクティブ: コンポーネントの初回マウント時に一度だけ実行される。依存しているシグナルの値が変化しても再実行されない。
用途: コンポーネントのライフサイクル中に一度だけ実行したい、バックグラウンドで継続的に動くタスク(例: WebSocket接続、定期的なデータ取得)に適している。
上の例では、200ms毎にcountを+1する。

3. use_context

コンポーネントツリーを介してデータを渡すために使われる。これにより、プロパティを何階層もバケツリレーで渡す必要がなくなる。ReactのuseContextに相当。
目的: 親コンポーネントから子孫コンポーネントへ、グローバルなデータを簡単に共有する。

例: router_restore_scroll.rs

enum Route {
    #[route("/")]
    Home {},
    #[route("/blog/:id")]
    Blog { id: i32 },
}

#[component]
fn App() -> Element {
    use_context_provider(|| Signal::new(Scroll::default()));
    //(1) アプリケーション全体で共有したいデータ(スクロール位置)をここで提供
    rsx! {
        Router::<Route> {}
    }
}

#[component]
fn Blog(id: i32) -> Element {
    rsx! {
        GoBackButton { "Go back" }
        div { "Blog post {id}" }
    }
}

type Scroll = Option<PixelsVector2D>;

#[component]
fn Home() -> Element {
    let mut element: Signal<Option<Rc<MountedData>>> = use_signal(|| None);
    let mut scroll = use_context::<Signal<Scroll>>();
//(2) 親コンポーネント(App)から提供されたデータをここで受け取る
    use_future(move || async move {  // 下記リファクタリング参照
        if let (Some(element), Some(scroll)) = (element.read().as_ref(), *scroll.peek()) {
            element
                .scroll(scroll, ScrollBehavior::Instant)
                .await
                .unwrap();
        }
    });

    rsx! { 
    ...

use_context_provider: 親コンポーネントでデータを提供する側
use_context: 子孫コンポーネントでデータを受け取る側
この関係は、ReactのContext APIと非常によく似ている。

なぜこれが必要か?
深い階層のコンポーネントにデータを渡したい場合、中間のコンポーネント全てにプロパティとしてデータを渡し続ける必要があるが、これは「プロパティのバケツリレー (prop drilling)」と呼ばれ、コードを複雑にする。Context APIは、この問題を解決する。
この例は、この仕組みをうまく利用して「ページ遷移前のスクロール位置を記憶し、戻ってきたときに復元する」機能を実現している。

・Appコンポーネント (提供する側)
アプリケーションのルートであるAppコンポーネントでuse_context_providerを呼び出している。
これにより、Signal(Scrollはスクロール位置PixelsVector2Dを保持する型)が、このAppコンポーネントのすべての子孫(つまり、アプリ内の全コンポーネント)からアクセス可能な状態になる。

・Homeコンポーネント (受け取る側)
Homeコンポーネント内でuse_contextを呼び出し、Appコンポーネントが提供したSignalを取得している。
ユーザーがリストをスクロールするとonscrollイベントが発火し、現在のスクロール位置をuse_contextで取得した共有Signalに保存する。
ユーザーがBlogページに遷移し、その後「戻る」ボタンでHomeコンポーネントに戻ってくると、use_futureが実行される。
use_futureは共有Signalに保存されているスクロール位置を読み取り、リストをその位置まで瞬時にスクロールさせる。
このように、use_context_providerとuse_contextを使うことで、HomeコンポーネントとBlogコンポーネントが直接やりとりすることなく、アプリケーション全体で状態(スクロール位置)を共有し、UI体験を向上させている。

・リファクタリング:返り値を使わないので、use_futureに変更した

-  _ = use_resource(move || async move {
+  use_future(move || async move {

以下のuse_future、use_resourceは、非同期処理やパフォーマンス最適化を扱うためのフック。これらはReactフックの概念をRustのリアクティビティモデルに適用したもの。

4. use_future

機能: コンポーネントのライフサイクルと結びつけられた非同期タスク(RustのFuture)を管理する。
目的: コンポーネントが初めてレンダリングされるときや、特定の依存データ(依存配列)が変更されたときに、一度だけ実行したい非同期処理(例えば、単純な計算やタイマー処理など)を実行し、その完了を待つために使用される。非リアクティブ。
例:上記の2つの例で説明済み

5. use_resource

機能: use_futureと同様に非同期タスクを管理するが、特に外部リソース(API、ファイルなど)のフェッチに特化しており、その状態を追跡する。返す値は、Resourceという列挙型で、Loading、Ready(データ)、Failed(エラー)のいずれかの状態を持つ。UIは、この状態(ステータス)に基づいてレンダリングを切り替える(例: Loadingならスピナー表示)。
目的: APIからのデータ取得など、ネットワークI/Oを伴う非同期操作を実行するために使用される。use_resourceは、非同期タスクの実行状態を自動的に追跡し、UIで「読み込み中」「成功」「エラー」の状態を簡単に表示できるようにする。
例: suspense.rs (cargo run --example suspense --features="http desktop")

fn app() -> Element {
    rsx! {
        div {
            h1 { "Dogs are very important" }
            p {
               ...
            }
            h3 { "Illustrious Dog Photo" }
            SuspenseBoundary {  // dioxusマジック
                fallback: move |suspense: SuspenseContext| suspense.suspense_placeholder().unwrap_or_else(|| rsx! {
                    div {
                        "Loading..."
                    }
                }),
                Doggo {}
            }

#[component]
fn Doggo() -> Element {
    let mut resource = use_resource(move || async move {  // 返り値のresourceを使って中断(suspend)する
        #[derive(serde::Deserialize)]
        struct DogApi {
            message: String,
        }

        reqwest::get("https://dog.ceo/api/breeds/image/random/")
            .await
            .unwrap()
            .json::<DogApi>()
            .await
    });

    // resource.suspend() を呼び出すと、use_resource が管理している非同期処理が完了するまで、コンポーネントのレンダリングを中断(サスペンド)する。
    let value = resource.suspend().with_loading_placeholder(|| {
        rsx! {
            div {
                "Loading doggos..."
            }
        }
    })?;

    let value = value.read();  // ソース変更箇所(下記リファクタリング参照)
    match &*value {
        Ok(resp) => rsx! {
            button { onclick: move |_| resource.restart(), "Click to fetch another doggo" }
            div { img { max_width: "500px", max_height: "500px", src: "{resp.message}" } }
        },
        Err(_) => rsx! {
            div { "loading dogs failed" }
            button {
                onclick: move |_| resource.restart(),
                "retry"
            }
        },
    }
}

リファクタリング:型安全性が低く、Dioxusのバージョンによってはコンパイルエラーになる可能性があるので変更した

- match value.read_unchecked().as_ref() {
+ let value = value.read();
+ match &*value

6. use_memo

機能: メモ化(Memoization)を行う、すなわち計算コストの高い関数の結果をキャッシュし、不要な再計算を防ぐ機能。同期処理に使う。ReactのuseMemoとほぼ同じ。
依存配列内の値が一つでも変更された場合のみ、内部の計算が再実行され、新しい結果が返される。
目的: コンポーネントの再レンダリングが発生しても、フックに渡した依存データが変わらない限り、高コストな計算(例:大きな配列のソート、複雑なデータ変換)をスキップするために使用される。これにより、UIの応答性が向上する。
例: todomvc.rs
以下では、todosというHashMapに対する集計やフィルタリング、ソートといった比較的重い処理をメモ化している

fn app() -> Element {
    // We store the todos in a HashMap in a Signal.
    // Each key is the id of the todo, and the value is the todo itself.
    let mut todos = use_signal(HashMap::<u32, TodoItem>::new);

    let filter = use_signal(|| FilterState::All);

    // We use a simple memoized signal to calculate the number of active todos.
    // Whenever the todos change, the active_todo_count will be recalculated.
    let active_todo_count =
        use_memo(move || todos.read().values().filter(|item| !item.checked).count());

    // We use a memoized signal to filter the todos based on the current filter state.
    // Whenever the todos or filter change, the filtered_todos will be recalculated.
    // Note that we're only storing the IDs of the todos, not the todos themselves.
    let filtered_todos = use_memo(move || {
        let mut filtered_todos = todos
            .read()
            .iter()
            .filter(|(_, item)| match filter() {
                FilterState::All => true,
                FilterState::Active => !item.checked,
                FilterState::Completed => item.checked,
            })
            .map(|f| *f.0)
            .collect::<Vec<_>>();

        filtered_todos.sort_unstable();

        filtered_todos
    });
...

以下では、todosという大きなSignal全体ではなく、特定のidを持つTodoアイテムのchecked状態とcontentsのみをメモ化している。これにより、todos全体のSignalが更新されたとしても、このTodoEntryコンポーネントが依存するcheckedやcontentsの値自体が変わっていなければ、TodoEntryコンポーネントの再描画を抑制することができる。これは、大規模なリストや頻繁に更新されるグローバルな状態を持つアプリケーションにおいて、子コンポーネントの不要な再描画を防ぐための非常に効果的なパターン。

/// A single todo entry
/// This takes the ID of the todo and the todos signal as props
/// We can use these together to memoize the todo contents and checked state
#[component]
fn TodoEntry(mut todos: WriteSignal<HashMap<u32, TodoItem>>, id: u32) -> Element {
    let mut is_editing = use_signal(|| false);

    // To avoid re-rendering this component when the todo list changes, we isolate our reads to memos
    // This way, the component will only re-render when the contents of the todo change, or when the editing state changes.
    // This does involve taking a local clone of the todo contents, but it allows us to prevent this component from re-rendering
    let checked = use_memo(move || todos.read().get(&id).unwrap().checked);
    let contents = use_memo(move || todos.read().get(&id).unwrap().contents.clone());

    rsx! {
        li {
            // Dioxus lets you use if statements in rsx to conditionally render attributes
            // These will get merged into a single class attribute
            class: if checked() { "completed" },
            class: if is_editing() { "editing" },
    ...

7. use_hook

機能:Dioxusの中でも特に基本的なフックの一つで、コンポーネントのライフサイクル中に一度だけクロージャを実行するために使用される。ReactのuseEffect(..., [])や、useRefの初期化部分に近い役割を果たす。
use_hookのクロージャは、コンポーネントが最初にレンダリングされるときに一度だけ実行され、その戻り値はコンポーネントの生存期間中ずっと保持される。再レンダリングが起きても、このクロージャが再実行されることはない。
目的:
a. 一度だけの初期化処理: コンポーネントがマウントされたときに一度だけ実行したい処理(例: ウィンドウの設定、トレイアイコンの初期化、イベントリスナーの登録など)。
b. 再レンダリングをトリガーしない状態の保持: 値が変更されてもUIの再描画を引き起こしたくない可変データを保持する場合(ReactのuseRefの用途に似ている)。
c. クリーンアップ処理: use_hookが返した値がDropトレイトを実装している場合、コンポーネントがアンマウントされるときにそのdropメソッドが自動的に呼ばれるので、リソースの解放処理を安全に行える。

それぞれの例:
a. 以下の例では、ウィンドウの設定やトレイアイコンの初期化といった、アプリケーションの起動時に一度だけ行えばよい処理をuse_hook内で行っている。もしuse_hookを使わずにこれらの処理を記述すると、appコンポーネントが再レンダリングされるたびに不要な処理が繰り返し実行されてしまう。
例: multiwindow_with_tray_icon.rs

fn app() -> Element {
    use_hook(|| {
        // この中のコードは`app`コンポーネントの初回レンダリング時に一度だけ実行される

        // メインウィンドウのクローズ挙動を設定
        window().set_close_behavior(WindowCloseBehaviour::WindowHides);

        // トレイアイコンを初期化
        init_tray_icon(default_tray_icon(), None)
    });

    rsx! {
        // ...
    }
}

b. 再レンダリングをトリガーしない状態の保持
use_signalは値の変更がUIの更新をトリガーするが、UIとは無関係な値を保持したい場合に、use_hookとRc<RefCell<T>>を組み合わせることで、ReactのuseRefのように使うことができる。
例:my_useRef_like.rs

use dioxus::prelude::*;
use std::cell::RefCell;
use std::rc::Rc;

fn app() -> Element {
    // use_hookで初期化することで、my_refは再レンダリングされても再生成されない
    let my_ref = use_hook(|| Rc::new(RefCell::new(0)));

    rsx! {
        button {
            onclick: move |_| {
                // .borrow_mut() を使って値を変更する
                // この変更は再レンダリングをトリガーしない
                *my_ref.borrow_mut() += 1;
                println!("現在の値: {}", my_ref.borrow());
            },
            "値をインクリメント(再描画なし)"
        }
    }
}

c. クリーンアップ処理
use_hookが返す値のdropがコンポーネントのアンマウント時に呼ばれることを利用して、クリーンアップ処理を実装できる。
例:my_cleanup.rs

use dioxus::prelude::*;

// この構造体のインスタンスが破棄されるときにメッセージを表示する
struct MyCleanup;

impl Drop for MyCleanup {
    fn drop(&mut self) {
        println!("コンポーネントがアンマウントされたので、クリーンアップ処理を実行します。");
    }
}

#[component]
fn MyComponent() -> Element {
    // MyCleanupのインスタンスを生成。このインスタンスはコンポーネントの生存期間中保持される。
    use_hook(MyCleanup);

    rsx! { "MyComponent" }
}

fn app() -> Element {
    let mut show = use_signal(|| true);

    rsx! {
        button { onclick: move |_| show.toggle(), "コンポーネントを切り替え" }
        if show() {
            MyComponent {}
        }
    }
}

最後の2つの例を実行するには、Cargo.tomlの最後に以下を追加する

[[example]]
name = "my_cleanup"
doc-scrape-examples = true

[[example]]
name = "my_useRef_like"
doc-scrape-examples = true

以上

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?