Open12
Yew で struct コンポーネント
なぜ struct コンポーネント?
yew はよくある React ライクなフロントエンド・フレームワークなので、React で馴染みの関数コンポーネントが、「分かりやすい」
しかし、実装してみると主に所有権の対応が面倒くさい。端的に言うとコードが clone
だらけになる。struct コンポーネントの方が、Rust のコードとしては書きやすいのではないか?
基本
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();
}
use_state を使わない (1)
コンポーネントの「状態」は struct のフィールドとして保持する。view
関数で仮想DOMを返却する時に、self.value
で値を出力する
use_state を使わない (2)
状態を更新する時には、仮想DOM から Component::Message::AddOne
を飛ばす。更新のロジックを update
関数に切り出す。view
は表示に集中する
状態がなければ関数コンポーネントの方が簡潔
親コンポーネントからプロパティを受け取って、それを表示するだけであれば、関数コンポーネントが適している。
yew-autoprops を使うことで、プロパティのための構造体を自前で作成する必要もなく、ほぼ普通に関数を書くだけでコンポーネントができる
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 から受け取ったデータをグローバルな状態に格納する
- エラーの処理は別のメッセージで飛んでくるので、適宜処理する(上では何もしてないけど)
グローバルステートを保存する
状態が変化したタイミングでサーバー側にその内容を投げて内容を保存したい。
このような場合は 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と併せて学習しています。
また参考にさせてください。