Open12

Yew で struct コンポーネント

koko_ukoko_u

まえがき

Rust のフロントエンド・フレームワーク yew のサンプル・コードで関数コンポーネントの例がほとんどなので、struct コンポーネントの使い方をメモする

koko_ukoko_u

なぜ struct コンポーネント?

yew はよくある React ライクなフロントエンド・フレームワークなので、React で馴染みの関数コンポーネントが、「分かりやすい」

しかし、実装してみると主に所有権の対応が面倒くさい。端的に言うとコードが clone だらけになる。struct コンポーネントの方が、Rust のコードとしては書きやすいのではないか?

koko_ukoko_u

基本

doc.rs の最初に書いてある。

わーい Elm Architecture

Sample Code
use yew::prelude::*;

enum Msg {
    AddOne,
}

struct App {
    value: i64,
}

impl Component for App {
    type Message = Msg;
    type Properties = ();

    fn create(ctx: &Context<Self>) -> Self {
        Self { value: 0 }
    }

    fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
        match msg {
            Msg::AddOne => {
                self.value += 1;
                true
            }
        }
    }

    fn view(&self, ctx: &Context<Self>) -> Html {
        html! {
            <div>
                <button onclick={ctx.link().callback(|_| Msg::AddOne)}>{ "+1" }</button>
                <p>{ self.value }</p>
            </div>
        }
    }
}

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

use_state を使わない (1)

コンポーネントの「状態」は struct のフィールドとして保持する。view 関数で仮想DOMを返却する時に、self.value で値を出力する

use_state を使わない (2)

状態を更新する時には、仮想DOM から Component::Message::AddOne を飛ばす。更新のロジックを update 関数に切り出す。view は表示に集中する

koko_ukoko_u

状態がなければ関数コンポーネントの方が簡潔

親コンポーネントからプロパティを受け取って、それを表示するだけであれば、関数コンポーネントが適している。

yew-autoprops を使うことで、プロパティのための構造体を自前で作成する必要もなく、ほぼ普通に関数を書くだけでコンポーネントができる

koko_ukoko_u

API 呼び出し

gloo_net を使う。

データを取得する関数を作る
pub async fn get_all_todo_list(config: &Config) -> AppResult<Vec<TodoItemDto>> {
    let url = format!("{}/rest/v1/todos", config.api.url);
    let headers = http::Headers::new();
    headers.append("apiKey", &config.api.key);
    headers.append("Authorization", &format!("Bearer {}", config.api.anon_key));
    let todo_list = http::Request::get(&url)
        .query([("select", "id,description,todo_status(status)")])
        .headers(headers)
        .send()
        .await
        .unwrap();
    let todo_list: Vec<TodoItemDto> = todo_list.json().await.unwrap();

    Ok(todo_list)
}
  • Config はAPI接続情報を持つとする
  • エラーハンドリングは省略
画面の初回表示のタイミングでデータを取得する
fn create(ctx: &Context<Self>) -> Self {
    let dispatch =
        Dispatch::<AppStore>::global().subscribe(ctx.link().callback(Self::Message::Store));

    let config = Config::init().unwrap();
    ctx.link().send_future(async move {
        match get_all_todo_list(&config).await {
            Ok(todo_list) => Self::Message::Fetched(todo_list),
            Err(error) => {
                log::error!("{error:?}");
                Self::Message::FetchError
            }
        }
    });

    Self { dispatch }
}
  • Scope::send_future で非同期な処理を呼び出せる
  • データを取得した後の処理はメッセージとして投げて、update 関数に任せる
update関数
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
    match msg {
        Self::Message::Fetched(todo_list) => {
            let todo_list = todo_list
                .into_iter()
                .map(|dto| (TodoId(dto.id), TodoItem::from(dto)))
                .collect::<BTreeMap<_, _>>();
            self.dispatch.set(AppStore { todo_list });
            true
        }
        Self::Message::FetchError => false,
        Self::Message::Store(_) => true,
    }
}
  • API から受け取ったデータをグローバルな状態に格納する
  • エラーの処理は別のメッセージで飛んでくるので、適宜処理する(上では何もしてないけど)
koko_ukoko_u

グローバルステート

struct コンポーネントで使えそうな状態管理のライブラリは Yewdux くらい?

使い方はこの辺 を参照。これ以外にはサンプルなさそう。

koko_ukoko_u

Dispatch::global() が見つからない

VSCode でコードを編集していると、Dispatch::global() が補完で出ないし、エラーになる。
ドキュメント を見ると、「wasm でのみ使える」と書いてある。

こんなんでわかるもんなの?

Hidden comment
koko_ukoko_u

rust-analyzer

rust-analyzer が今 wasm のコードを書いているとわからないと、VSCode 上でエラーになる。次の設定をワークスペースの設定などに入れる

{
    "rust-analyzer.cargo.target": "wasm32-unknown-unknown"
}
koko_ukoko_u

グローバルステートを保存する

状態が変化したタイミングでサーバー側にその内容を投げて内容を保存したい。
このような場合は Storeトレイトを時前で実装する

Store を手動で実装する
#[derive(Debug, Clone, PartialEq, Default)]
pub struct AppStore {
    pub todos: Vec<Todo>,
}

impl Store for AppStore {
    fn new(ctx: &Context) -> Self {
        init_listener(StoreDbListner, ctx);

        Self::default()
    }

    fn should_notify(&self, old: &Self) -> bool {
        self != old
    }
}
  • init_listener で状態が変化した時に何をしたいかを登録することができる
Listener の実装
pub struct StoreDbListener;

impl Listener for StoreDbListener {
    type Store = AppStore;

    fn on_change(&mut self, _ctx: &Context, state: Rc<Self::Store>) {
        let todos = state.todos.clone();
        spawn_local(async move {
            let _ = api::store(&todos).await;
        });
    }
}
  • wasm-bindgen-futures にある spawn_local を使って非同期な関数を呼び出す。
  • api::store() はバックエンドのAPIを呼び出してデータを保存するものとする(なんだかいい感じにupsertする実装がされている)
いなのいなの

趣味です。actixwebと併せて学習しています。
また参考にさせてください。