1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

ジョブカンAdvent Calendar 2023

Day 8

【Rust】Yewを使ってフロント実装します。その1: 使い方編

Last updated at Posted at 2023-12-07

こんにちは!

前回は Rust と Yew を使って開発を始める準備を行いましたね!
今回のアーティクルでは開発を進めるためのヒントや Yew の一部機能を紹介します。

ではさっそくやっていきましょう!

基本操作

Console

Yew は WASM にコンパイルしますが、 DOM を操作して動的な Web ページを作ることができるクレートです。

オブジェクトを出したり消したり、時には少し複雑な計算をしたり、そんな時には操作の過程で途中の値や状態を出力したい場合があります。 Javascript では console を使用してブラウザコンソールから確認する方法がよく知られていますね。 Yew 上で console を使用したいときは、 gloo クレートを使用します。

gloo crate追加
cargo add gloo
main.rs
use gloo::console;
use yew::prelude::*;

#[function_component]
fn App() -> Html {
    console::log!("Hello log!");
    console::debug!("Hello debug!");
    console::info!("Hello info!");
    console::warn!("Hello warn!");
    console::assert!(true, "Hello world!");
    console::assert!(false, "This will fail!");
    console::table!([0, 1, 2], ["a", "b"]);
    html! {
        <div>
            {"hello yew"}
        </div>
    }
}

fn main() {
    yew::Renderer::<App>::new().render();
}

glooconsole モジュールは Javascript における console のようにブラウザコンソールを操作するためのものです。 console のすべての機能が再現されているわけではありませんが、これを使えばデバッグ作業も捗るでしょう!
他にどのような機能を持ち、どのように使うのかは API のリファレンスを確認してみてください!

gloo クレートは Rust で WASM 開発を行うための様々な機能を提供するため、他のモジュールについても調べてみるととても役に立つでしょう!

プロパティによるイベントハンドリング

オブジェクトに対するイベントを引き金に何かアクションを起こしたいときはイベントトリガープロパティを使用します。

ボタンを押した時にアクションを起こしたいときは onclick プロパティを使用します。

onclick
use gloo::console;
use yew::prelude::*;

#[function_component]
fn App() -> Html {
    let click = |_| console::log!("button is clicked");

    html! {
        <button onclick={click}>
            { "Click me!" }
        </button>
    }
}

fn main() {
    yew::Renderer::<App>::new().render();
}

ここで onclick プロパティに渡している click は引数を1つ受け取る(そして引数を使わない)クロージャです。ボタンがクリックされると MouseEvent を受け取り、 click が実行されます。

プロパティ名は onclick のように全て小文字にする必要があります。 onClick のようなキャメルケースだと型エラーになってしまいます。

文字の入力欄が変更されるたびにアクションを起こしたいときは oninput プロパティを使用します。

web-sys crate追加
cargo add web-sys
oninput
use gloo::console;
use web_sys::HtmlInputElement;
use yew::prelude::*;

#[function_component]
fn App() -> Html {
    let text_change = {
        move |event: InputEvent| {
            console::log!(event
                .target_dyn_into::<HtmlInputElement>()
                .expect("cast failed")
                .value());
        }
    };

    html! {
        <input type="text" value={ "edit here!" } oninput={text_change} />
    }
}

fn main() {
    yew::Renderer::<App>::new().render();
}

イベントによってはイベントハンドラに明示的に受け取るイベント型を指定する必要があるので注意しましょう。
イベントからDOM要素を取り出す場合は要素型の情報も必要になるので、必要に応じて web_sys クレートも追加して使用しましょう。

イベントリスナープロパティやイベント型の情報一覧は Yew のドキュメントに記述されているので確認してみましょう!

フラグメント <></>

Yew でコンポーネントを記述していると、複数のコンポーネントを並べて返したいときがあります。

コンパイルエラー
use yew::prelude::*;

#[function_component]
fn App() -> Html {
    html! {
        <div>
            {"Hello"}
        </div>
        <div>
            {"Yew"}
        </div>
    }
}

fn main() {
    yew::Renderer::<App>::new().render();
}

しかし、一つのコンポーネントのみを受け渡しするべき場所で複数のコンポーネントを並べてしまうとコンパイルエラーとなってしまいます。
複数のコンポーネントを並べて受け渡ししたいときは、フラグメント <></> で囲うようにしましょう。

フラグメント <>< / > で囲う
use yew::prelude::*;

#[function_component]
fn App() -> Html {
    html! {
        <>
            <div>
                {"Hello"}
            </div>
            <div>
                {"Yew"}
            </div>
        </>
    }
}

fn main() {
    yew::Renderer::<App>::new().render();
}

yew::functional hooks

Yew で DOM 操作をするときには、ストラクトコンポーネント、または関数コンポーネントを作成して用いることになります。

ストラクトコンポーネントはその名の通り、ストラクトなのでフィールドを保持してコンポーネントの状態を管理することができます。

一方、関数コンポーネントは最後にHTMLオブジェクトを返す関数なので、フィールドや変数を保持することができません。
つまり、関数コンポーネントでは状態を保持することができないのですが、 yew::functional モジュールには関数コンポーネントでも状態を保持するための機能が用意されています。
関数コンポーネントで状態を管理するための機能 hook をいくつか紹介します。

use_state

ボタンを押すたびに表示するカウントを 1 ずつ増やしたり、 2 倍にしたりするアプリケーションを作りたいとします。
そして次のように書いてみました。

use yew::prelude::*;

#[function_component]
fn App() -> Html {
    let mut count = 0_i128;
    let plus = { move |_| count = count.saturating_add(1) };
    let double = { move |_| count = count.saturating_mul(2) };

    html! {
        <div>
            <button onclick={ plus }>{"+1"}</button>
            <button onclick={ double }>{"x2"}</button>
            <p>{ count }</p>
        </div>
    }
}

fn main() {
    yew::Renderer::<App>::new().render();
}

この例では(そもそも型制約でコンパイルできないのですが、動作したとしても) App コンポーネントは関数コンポーネントなので描画される度に count は 0 になってしまいます。
(さらに言えば、再描画すらされないので絶対に表示内容が変わりません。)

そこで、利用できるのが use_state です。
countuse_state を使って状態として管理することで、 count を設定する度に描画が実行され、 値が 0 になることもなくなります。

use_state
use yew::prelude::*;

#[function_component]
fn App() -> Html {
    let count = use_state(|| 0_i128);
    let plus = {
        let count = count.clone();
        move |_| count.set(count.saturating_add(1))
    };
    let double = {
        let count = count.clone();
        move |_| count.set(count.saturating_mul(2))
    };

    html! {
        <div>
            <button onclick={ plus }>{"+1"}</button>
            <button onclick={ double }>{"x2"}</button>
            <p>{ *count }</p>
        </div>
    }
}

fn main() {
    yew::Renderer::<App>::new().render();
}

use_state の引数は 初期値を返す関数 で、 UseStateHandle を返します。
UseStateHandleset メソッドを持っており、 set メソッドに値を渡して実行すると UseStateHandle が保持する値を変更することができます。
UseStateHandle が保持する値は Deref * で参照することができます。

また、 set メソッドが実行されると、その UseStateHandle を持つコンポーネントが再描画されます。
「+1」ボタンを押すと count に 1 を加算した値が set され、 count が保持する値が変更されると同時に再描画されます。すると、再描画時の App の実行では count の値が set した値になるので、 p タグの中身も変更されます。

この hook のおかげで、関数コンポーネントでも状態を保持することができるようになります!
関数コンポーネントでは状態を管理するためにさまざまな hook を利用しなければいけないので面倒に感じるかもしれません。 しかし、ストラクトコンポーネントでは自身に状態を持ち続け、 Rust における所有権やライフタイムのシステムを含めて状態を管理しなければならないため、関数コンポーネントを利用した方がコードの記述を煩雑にせずに済ませやすくなります。

use_effect

次に紹介するのは use_effect です。
use_effect はコンポーネントがマウントされた時と再描画が起きた時に実行される関数を登録するための hook です。

use_effect
use gloo::console;
use yew::prelude::*;

#[function_component]
fn App() -> Html {
    let count0 = use_state(|| 0_i128);
    let plus = {
        let count = count0.clone();
        move |_| count.set(count.saturating_add(1))
    };

    let count1 = use_state(|| 1_i128);
    let double = {
        let count = count1.clone();
        move |_| count.set(count.saturating_mul(2))
    };

    use_effect(|| {
        console::log!("effect");
    });

    html! {
        <div>
            <button onclick={ plus }>{"+1"}</button>
            <p>{ *count0 }</p>
            <button onclick={ double }>{"x2"}</button>
            <p>{ *count1 }</p>
        </div>
    }
}

fn main() {
    yew::Renderer::<App>::new().render();
}

use_effect の引数は 実行する関数 で、コンポーネントがマウントされた時と再描画が起きた時に実行されます。
上記の例では、 App コンポーネントがマウントされた時とボタンを押した時に console::log!("effect") が実行されます。
ブラウザコンソールを開きながらページをリロードしたり、ボタンを押して試してみてください。

再描画が起きる度に実行ではなく、特定の状態が変更された時に実行したい場合は use_effect_with を使用します。

use_effect_with
use gloo::console;
use yew::prelude::*;

#[function_component]
fn App() -> Html {
    let count0 = use_state(|| 0_i128);
    let plus = {
        let count = count0.clone();
        move |_| count.set(count.saturating_add(1))
    };

    let count1 = use_state(|| 1_i128);
    let double = {
        let count = count1.clone();
        move |_| count.set(count.saturating_mul(2))
    };

    use_effect_with(*count1, |v| {
        console::log!(format!("effect: {}", *v));
    });

    html! {
        <div>
            <button onclick={ plus }>{"+1"}</button>
            <p>{ *count0 }</p>
            <button onclick={ double }>{"x2"}</button>
            <p>{ *count1 }</p>
        </div>
    }
}

fn main() {
    yew::Renderer::<App>::new().render();
}

use_effect_with の第1引数は 変更を検知したい値で、第2引数は 実行する関数 です。
第2引数に指定する関数は、変更された第1引数に指定した値を受け取って実行されます。

ブラウザコンソールを開きながらボタンを押して、「x2」ボタンを押したときだけ実行されることを確かめてください。

use_effect_with ではコンポーネントがマウントされた最初の描画時のみ実行されるようにすることもできます。
その場合は () を第1引数へ渡します。

また、実行する関数からの戻り値として関数を返すと、コンポーネントがアンマウントされる時にその関数が実行されます。

use_effect_with init and destroy
use gloo::console;
use yew::prelude::*;

#[function_component]
fn UseEffect() -> Html {
    let count = use_state(|| 0_i128);
    let plus = {
        let count = count.clone();
        move |_| count.set(count.saturating_add(1))
    };

    use_effect_with((), |v| {
        console::log!(format!("effect: {:?}", v));

        || console::log!("cleanup")
    });

    html! {
        <div>
            <button onclick={ plus }>{"+1"}</button>
            <p>{ *count }</p>
        </div>
    }
}

#[function_component]
fn App() -> Html {
    let flag = use_state(|| false);
    let toggle = {
        let flag = flag.clone();
        move |_| flag.set(!*flag)
    };

    html! {
        <div>
            <p>{ "check on console" }</p>
            <button onclick={ toggle }>{ if *flag { "hide" } else { "show" } }</button>
            if *flag { <UseEffect /> }
        </div>
    }
}

fn main() {
    yew::Renderer::<App>::new().render();
}

ブラウザコンソールを開きながらボタンを押してみてください。
カウンターが出現する時に effect が実行され、カウンターが消える時に cleanup が実行されることが確認できます。

初期状態を設定する時に何か処理が必要な場合や、コンポーネントがアンマウントされる時に何か処理をしたい場合はこのように use_effect_with を使用しましょう。

use_context

外部からコンポーネントにデータを渡すには、通常コンポーネントのプロパティを経由します。
しかし、コンポーネントの階層が深くなると、親コンポーネントから子コンポーネント、孫コンポーネントとデータを渡すのが面倒になってきます。
そんな時に便利なのが use_context です。

use_context
use yew::prelude::*;

#[function_component]
fn GrandchildComponent() -> Html {
    let string = use_context::<String>().expect("no context found");

    html! {
        <>
            <p> { "there is in grandchild" } </p>
            <p> { string } </p>
        </>
    }
}

#[function_component]
fn ChildComponent() -> Html {
    let string = use_context::<String>().expect("no context found");

    html! {
        <>
            <p> { "there is in child" } </p>
            <p> { string } </p>
            <GrandchildComponent />
        </>
    }
}

#[function_component]
fn App() -> Html {
    let string = String::from("how are you?");

    html! {
        <>
            <p> { string.clone() } </p>
            <ContextProvider<String> context={string}>
                <ChildComponent />
            </ContextProvider<String>>
        </>
    }
}

fn main() {
    yew::Renderer::<App>::new().render();
}

ContextProvideruse_context で使用するコンテキストを提供するコンポーネントです。
ContextProvider で囲まれていれば子コンポーネントでも孫コンポーネントでも use_context でコンテキストを受け取ることができます!

ContextProvider には use_context で受け取るコンテキストの型を指定する必要がありますが、この型には ClonePartialEq トレイトが実装されている必要があります。
自分で作成したストラクトをコンテキストに指定したい場合は ClonePartialEq トレイトを実装し忘れないように注意しましょう。

ContextProvideruse_context を用いればPropsを経由せずに子孫コンポーネントでデータを受け取ることができて便利な反面、規模が大きく複雑なプロジェクトになるとどこで提供されたコンテキストなのか分かりづらくなり可読性が悪くなってしまう場合もあるので気を付けましょう。

use_reducer

最後に紹介するのは use_reducer です。

use_reduceruse_state と似ていて、コンポーネントの状態を管理するための hook です。
use_state とは違い、 use_reducer では任意の値ではなく Reducible トレイトを実装したストラクトを作成して渡す必要があります。
つまり、 use_reducer では状態を更新するための処理をストラクトが持つことになるという違いもあります。

use_reducer
use std::rc::Rc;

use yew::prelude::*;

enum ReducerAction<T> {
    Increment,
    Add(T),
}

#[derive(Default)]
struct ReducerState {
    count: i128,
}

impl Reducible for ReducerState {
    type Action = ReducerAction<i128>;

    fn reduce(self: Rc<Self>, action: Self::Action) -> Rc<Self> {
        let next_count = match action {
            ReducerAction::Increment => self.count.saturating_add(1),
            ReducerAction::Add(int) => self.count.saturating_add(int),
        };

        Self { count: next_count }.into()
    }
}

#[function_component]
fn App() -> Html {
    let counter = use_reducer(ReducerState::default);

    let increment = {
        let counter = counter.clone();
        move |_| counter.dispatch(ReducerAction::Increment)
    };
    let add_three = {
        let counter = counter.clone();
        move |_| counter.dispatch(ReducerAction::Add(3))
    };

    html! {
        <div>
            <button onclick={ increment }>{"+1"}</button>
            <button onclick={ add_three }>{"+3"}</button>
            <p>{ counter.count }</p>
        </div>
    }
}

fn main() {
    yew::Renderer::<App>::new().render();
}

use_state では任意の値を set することで状態を変更していましたが、 use_reducer では dispatchAction を渡すことで状態を変更します。
Action によって更新の処理を切り替えることで、 use_reducer では限られたインターフェースを提供しながら複雑な状態の更新を行うことができます。
Action の列挙型が値を持つように実装すれば、その値を使って dispatch を実行することもできます。


例をたくさん考えたので長くなってしまいましたね。
他にもいくつかの hooks が用意されています。詳しくはドキュメントを確認してみてください!


関数コンポーネントに use_stateuse_effect ... 気付いた方も多いかと思いますが、 Yew は React にとてもよく似ています。 React での開発経験がある方は Yew での開発も行いやすいでしょう!

しかし、 現状では Yew で開発を行うにはいくつか問題もあります。

  • React の機能を使ったライブラリは代替できない
    Javascript における多くのライブラリは Rust のクレートでまかなえるでしょう。 しかし、 React の機能を必要とするライブラリを使いたくなったときは Yew を使って自分で実装を再現するか、誰かが実装して公開されるのを待つしかありません。
  • ESLint のように html! マクロ内をフォーマットする方法がない
    html! マクロ内には Rust と HTML 文法が混在した記述になりますが、このマクロ内のコードをフォーマットする方法は今のところありません。
  • 正式なリリースバージョンではないので破壊的な変更が行われる可能性がある
    このアーティクル執筆時点では Yew の最新バージョンは 0.21.0 です。 例えば use_effect_with はバージョン 0.20.0 では use_effect_with_deps でしたし、 hook の引数の順番もいくつか変更されました。 バージョンアップの度に API へ破壊的な変更を行われる可能性があるため、現状の Yew を利用してプロジェクトを進めるとマイグレーションにかかるコストが増大してしまう危険性があります。

このようにいくつか懸念もありますが、 Yew は公式ドキュメントが充実しており、マイグレーションガイドも公開されています。
正式に 1.0.0 がリリースされれば Yew を利用した拡張コンポーネントクレートの開発とメンテナンスも安定していくことでしょう。
将来に期待できますね!


不定期の更新になりますが、何か簡単な機能を持った(だけど便利で誰かの役に立つ)アプリケーションを開発するところまで連載したいと思っています。
アイディア募集中です!よろしくお願いいたします!
ありがとうございました!


この文章の一部は Chat-GPT 3.5 によって和文校正されています。

お知らせ

DONUTSでは新卒中途問わず積極的に採用活動を行っています。
詳細はこちらをご確認ください。

ジョブカン事業部のエンジニア募集はこちら。

1
0
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
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?