🧊

Rust x WASMのyewでTodoAppを作ってみた

話題になってたのは去年くらいな気もするし今さら感はあるけど、今だからこそすごい化けてたりしないかなーという期待も込めて。

個人的には、WASMでWebアプリを作る時代になるとはあまり思えてないけど、まぁ試しておく価値はあるかなと思いその学びをメモ。

yewstack/yew

GitHub - yewstack/yew: Rust / Wasm framework for building client web apps

RustでReactっぽいコードが書けて、それがWASMで動くので、型ありでちょっぱやなWebアプリが作れるぜ!っていうやつ。

今のバージョンは`0.11.0`で、最小のコードはこんな感じ。

use yew::{html, Callback, ClickEvent, Component, ComponentLink, Html, ShouldRender};

struct App {
    clicked: bool,
    onclick: Callback<ClickEvent>,
}

enum Msg {
    Click,
}

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

    fn create(_: Self::Properties, link: ComponentLink<Self>) -> Self {
        App {
            clicked: false,
            onclick: link.callback(|_| Msg::Click),
        }
    }

    fn update(&mut self, msg: Self::Message) -> ShouldRender {
        match msg {
            Msg::Click => {
                self.clicked = true;
                true // Indicate that the Component should re-render
            }
        }
    }

    fn view(&self) -> Html {
        let button_text = if self.clicked { "Clicked!" } else { "Click me!" };

        html! {
            <button onclick=&self.onclick>{ button_text }</button>
        }
    }
}

fn main() {
    yew::start_app::<App>();
}

なるほど。

  • コンポーネント志向で使える
  • `html!`マクロでJSX的な記述ができる
  • `link.callback()`で`Message`を返すことで、Flux的に状態管理もできる
  • Rustなので基本的にカチカチの型がつく

React x TypeScriptの現環境と比べると、おそらく同等の使用感とは言えないと思うけど、たしかに可能性は感じるかもしれない・・w

コンパイルするとWASMをロードするグルーコードを含んだJSファイルができて、それをロードするだけ。

やってみた

GitHub - leader22/yew-todo

公式にもTodoMVCのサンプルがあるけど、1ファイルで完結してたりとreal-worldではないよな・・と思ったので、そこを重点的にカバーしつつ、最低限の機能だけ実装した。

そして得られた学びが以下。

ファイルを分割する

これはRust本体の話で、別の記事にした。

lealog.hateblo.jp

Statefullコンポーネント

pub struct App {}

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

    fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {}

    fn update(&mut self, msg: Self::Message) -> ShouldRender {}

    fn view(&self) -> Html {}
}

(今や一線を退いたはずの)Reactのclassコンポーネントみたいなもので、`Component`たるためにはこれだけ定義が必要。

Hooksとかはないので、状態を持つコンポーネントの場合は、このスタイルでやるしかない。

このスタイルで定義したコンポーネントは、`view()`の中で`<App />`形式で書ける。

Statelessコンポーネント

状態を持たない場合は、コンポーネントというか、ただの`Html`を返す関数にしてしまえばいい。

fn render_counter(done_len: &usize, total_len: &usize) -> Html {
    html! {
        <p>{format!("{}/{} todo(s) are done!", done_len, total_len)}</p>
    }
}

こっちのスタイルの場合は、`<App />`的な感じでは書けず、`{render_counter(3, 5)}`みたくただの関数になっちゃう。

関数をpropsで渡す

配列をリストにして、各アイテムにイベントハンドラを設定したい場合など。

即時関数はRustでいうクロージャで、それを引数に取るためにはちょっと遠回りな型をつける必要がある・・。

pub fn render_list<F>(todos: &Vec<Todo>, on_click_done: F) -> Html
where
    F: Fn(usize) -> Callback<ClickEvent>,
{
  html! {
    // ...
  }
}

そもそもRustに慣れてないと、何をどうすればいいんや・・ってなる。(なった)

PropertiesはPropsではない

ReactのPropsと同じ感覚では使えない。

イメージとしては、いったん自身で参照を確保して、それを使うって感じ・・?

#[derive(Clone, PartialEq, Properties)]
pub struct Props {
    pub title: String,
    #[props(required)]
    pub onsignal: Callback<()>,
}

pub struct Button {
    link: ComponentLink<Self>,
    // なので重複っぽくなる
    title: String,
    onsignal: Callback<()>,
}

impl Component for Button {
    type Message = Msg;
    type Properties = Props;

    fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
        // いったん自身で保持する
        Button {
            link,
            title: props.title,
            onsignal: props.onsignal,
        }
    }
}

ドキュメントにもそのオーバーヘッドについては記述があった。

Properties - Docs

`callback.emit()`

親のコンポーネントから受け取ったハンドラを、子コンポーネント側で呼びたいとき。

`html!`内で展開するHTMLの`onclick`ハンドラなどは、`Callback`になってる必要があって、外側から受け取ったハンドラに自身の状態をあわせて返すの無理では?!ってなった。

具体的には、Todoの入力欄で追加ボタンを押したときに、その入力内容を親に伝えたかった。

fn update(&mut self, msg: Self::Message) -> ShouldRender {
    match msg {
        Msg::Update(val) => self.value = val,
        Msg::Add => {
            // ここで紐付ける
            self.on_add.emit(self.value.clone());
            self.value.clear();
        }
    }
    true
}

fn view(&self) -> Html {
    html! {
        <div>
            <input
                type="text"
                value={&self.value}
                oninput=self.link.callback(|e: InputData| Msg::Update(e.value))
            />
            // いつもどおりメッセージングだけする
            <button onclick=self.link.callback(|_| Msg::Add)>{"add"}</button>
        </div>
    }
}

コンポーネントでのイベントハンドラはいつもどおり自身に対するメッセージングにして、そっちで処理する。
そして`Callback`には`emit()`という関数があるので、それを使えばいけた。

こうなってくるとやっぱ`EventEmitter`的なのが欲しくなるな・・と思ったけど今回は追わなかった。(そういう機能ありそう)

おわりに

React x TypeScriptと比べるとまだまだではあるけど、どっちか選べ!って話ではないので、当初の想定通りだったというだけ。

今回はまったく振れてないけどCSSどうするんだ問題とかもまだあるし。

Styling Brainstorming · Issue #533 · yewstack/yew · GitHub

今度は`wasm-bindgen`側のこういうの、あれば試してみたいなーと思ってます。