YewでVecの中身をサブコンポーネントを使って表示する際のコツ

Yewでlistを別のコンポーネントに渡して処理する

Vec型のデータ(つまりリスト)を別のComponentに渡して描画する、というのは頻出のパターンですね。

普通に考えたら特に難しくない内容ですので、とりあえずはまず普通に動作する全体コードを以下に記します。
親コンポーネントでデフォルト値としてvec![1,2,3]を持っていて、その値をそれぞれSubComponentに渡して描画しています。
ADDボタンをクリックすると現在のVecの中身に+1して要素を追加する、というものです。

use web_sys::MouseEvent;
use yew::{html, Component, Context, Html, Properties};

#[derive(Clone, Debug)]
pub struct SubComponent {
    value: i32,
}

pub enum SubMsg {
    OnClick(i32),
}

#[derive(Properties, Debug, PartialEq)]
pub struct SubProp {
    pub value: i32,
}

impl Component for SubComponent {
    type Message = SubMsg;
    type Properties = SubProp;

    fn create(ctx: &Context<Self>) -> Self {
        let value = ctx.props().value;
        Self { value }
    }
    fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
        match msg {
            SubMsg::OnClick(v) => {
                log::info!("{}がクリックされたよ(サブコンポーネント)", v);
                false
            }
        }
    }

    fn view(&self, ctx: &Context<Self>) -> Html {
        let value = self.value;
        let onclick = ctx
            .link()
            .callback(move |_e: MouseEvent| SubMsg::OnClick(value));
        html! {
            <div>
                <span>{format!("サブコンポーネントでVecの中身の表示:{}", value)}</span><br />
                <button {onclick}>{"click"}</button>
            </div>
        }
    }
}

#[derive(Clone, Debug)]
pub struct Model {
    data: Vec<i32>,
}

pub enum Msg {
    Add,
}

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

    fn create(_ctx: &Context<Self>) -> Self {
        Model {
            data: vec![1, 2, 3],
        }
    }

    fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
        match msg {
            Msg::Add => {
                let next_value = self.data.len() + 1;
                self.data.push(next_value as i32);
                true
            }
        }
    }

    fn view(&self, ctx: &Context<Self>) -> Html {
        let list = self
            .data
            .iter()
            .map(|value| {
                html! {
                    <p>
                    <span>{format!("親コンポーネントでVecの中身の表示:{}", value)}</span>
                    <SubComponent key={value.to_string()} {value} />
                    <hr />
                    </p>
                }
            })
            .collect::<Vec<Html>>();

        html! {
        <>
            {list}
            <button onclick={ctx.link().callback(|_e: MouseEvent|Msg::Add)}>{"ADD"}</button>
        </>
        }
    }
}

これでアクセスすると

https://doitu.info/images/blog/rust-yew-1.png

と表示されます。
この状態でADDをクリックすると、

https://doitu.info/images/blog/rust-yew-2.png

問題なく値が追加されました。

落とし穴

上記の親コンポーネント側に記述している、<SubComponent key={value.to_string()} {value} />の部分のkey={value.to_string()}が無いと、値を動的に追加する際に描画される値がおかしくなります。
もしkeyを指定しなくてもコード自体はエラーなくコンパイルも実行もできますが、そうすると動的にself.dataの中身を追加した時のSubComponentの再描画がおかしな挙動をします。
具体的には以下のようにずれた内容で表示されてしまいます。

この状態で、

https://doitu.info/images/blog/rust-yew-3.png

ADDをクリックすると明らかに表示内容がおかしいことに

https://doitu.info/images/blog/rust-yew-4.png

そもそもこの件に関しては、SubComponentに渡すクロージャに持たせた値がクロージャ実行時に明らかに意図した値じゃないぞ?ということから調査を始めました。
公式ドキュメントには、このkeyに関しての記述は有りますが、Yewが内部的に速度などを最適化するために利用する、と説明されていて、指定しないと描画がおかしくなる、というドンピシャな説明はされていません。
ただし最後の説明で再利用が云々とされているのでおそらく想定される動作なのかなとは思います。

公開日:2023/05/25

Rust Yew

About me

ドイツの現地企業でWeb Developer/System Administratorとして働いているアラフォーおじさんです。

プログラミングとかコンピュータに関する事がメインですが、日常的なメモとか雑多なことも書きます。

Links :
目次

Yewでlistを別のコンポーネントに渡して処理する


落とし穴