4

I have a yew struct component that should make a get request to the api and then render the list of items. I'm trying to perform the request inside the render method of the component and I'm running into lifetime issues where I can't use the reference to self within the wasm_bindgen_future. I have to use the wasm_bindgen_future in order to execute the async api request. Here is the code (roughly)

pub struct ViewLessonPlans {
    lesson_plans: Vec<LessonPlan>,
    loading_condition: ComponentLoadingStage
}

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

    fn create(ctx: &Context<Self>) -> Self {
        Self {
            lesson_plans: vec![],
            loading_condition: ComponentLoadingStage::Loading
        }
    }

    fn view(&self, ctx: &Context<Self>) -> Html {

        match self.loading_condition {
            ComponentLoadingStage::Loading => {
                html! { <h1>{"Lesson Plans Loading"}</h1>}
            },
            ComponentLoadingStage::Success => {
                self.lesson_plans.iter().map(|lp| {
                    html! { <ViewLessonPlan lesson_plan={lp.clone()} /> }
                }).collect::<Html>()
            },
            ComponentLoadingStage::Error => {
                html! { <h1>{ "There was an error loading the lesson plans!" }</h1>}
            },
        }
    }

    fn rendered(&mut self, ctx: &Context<Self>, first_render: bool) {
        if first_render {
            wasm_bindgen_futures::spawn_local(async move {
                match get_lesson_plans().await {
                    Ok(lesson_plans) => {
                        self.lesson_plans = lesson_plans.iter().map(|(_id, lp)| {
                            lp.clone()
                        }).collect();
                        self.loading_condition = ComponentLoadingStage::Success;
                    },
                    Err(_) => {
                        self.loading_condition = ComponentLoadingStage::Error;
                    },
                }
            });
        }
    }
}

and the generated error

`self` has an anonymous lifetime `'_` but it needs to satisfy a `'static` lifetime requirement
...is used here...rustcE0759

How can I make this api request and use the response to update self?

Edit: For reference, this is my function_component version of the desired functionality. Annoyingly it displays the Error case briefly on refresh, not sure why. I've done a little refactoring of ComponentLoadingStage so that the success variant can simply contain the api response content to make things simpler.

#[function_component(ViewLessonPlans)]
pub fn view_lesson_plans() -> Html {
    // Setup the state
    let state_init: UseStateHandle<ComponentLoadingStage<Vec<LessonPlan>>> =
        use_state(|| ComponentLoadingStage::Loading);
    let state = state_init.clone();

    // Perform the API request
    wasm_bindgen_futures::spawn_local(async move {
        match get_lesson_plans().await {
            Ok(lesson_plans) => {
                state.set(ComponentLoadingStage::Success(
                    lesson_plans.iter().map(|(_id, lp)| lp.clone()).collect(),
                ));
            }
            Err(_) => {
                state.set(ComponentLoadingStage::Error);
            }
        }
    });

    // Return the view
    match (*state_init).clone() {
        ComponentLoadingStage::Loading => {
            html! { <h1>{"Lesson Plans Loading"}</h1>}
        }
        ComponentLoadingStage::Success(lesson_plans) => lesson_plans
            .iter()
            .map(|lp| {
                html! { <ViewLessonPlan lesson_plan={lp.clone()} /> }
            })
            .collect::<Html>(),
        ComponentLoadingStage::Error => {
            html! { <h1>{ "There was an error loading the lesson plans!" }</h1>}
        }
    }
}

1 Answers1

2

when creating an async block, whatever is in it will maybe be thrown to another thread, or could be called in 2 years. That's why rust don't let you move refs into them other than static ones.

the spawn_local() function specifically state:

The future must be 'static because it will be scheduled to run in the background and cannot contain any stack references.

So you can't use a mutable reference to self, but there is a way my friend!

Yew know that in the context of UI you need a way to send informations to yourself, at an unknown point, like a button click.

There is the update methods that Yew let you create, that accept a message and you can react to those message.

So what you need to do is create a link, move it to the async block, call the get_lesson_plans() method, and use the link to send your message. You will then receive your result in the update method, which has access to a mutable reference to self.

it would look like that:

impl Component for ViewLessonPlans {
    type Message = Result<..., ...>; // plug return type of get_lesson_plans()
    type Properties = ();

    fn create(ctx: &Context<Self>) -> Self {
        Self {
            lesson_plans: vec![],
            loading_condition: ComponentLoadingStage::Loading
        }
    }

    fn view(&self, ctx: &Context<Self>) -> Html {

        match self.loading_condition {
            ComponentLoadingStage::Loading => {
                html! { <h1>{"Lesson Plans Loading"}</h1>}
            },
            ComponentLoadingStage::Success => {
                self.lesson_plans.iter().map(|lp| {
                    html! { <ViewLessonPlan lesson_plan={lp.clone()} /> }
                }).collect::<Html>()
            },
            ComponentLoadingStage::Error => {
                html! { <h1>{ "There was an error loading the lesson plans!" }</h1>}
            },
        }
    }

    fn rendered(&mut self, ctx: &Context<Self>, first_render: bool) {
        if first_render {
            let link = ctx.link().clone();
            wasm_bindgen_futures::spawn_local(async move {
                let result = get_lesson_plans().await;
                link.send_message(result);
            });
        }
    }

    fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
        match msg {
            Ok(lesson_plans) => {
                self.lesson_plans = lesson_plans.iter().map(|(_id, lp)| {
                    lp.clone()
                }).collect();
                self.loading_condition = ComponentLoadingStage::Success;
            },
            Err(_) => {
                self.loading_condition = ComponentLoadingStage::Error;
            },
        }
        true
    }
}
Bamontan
  • 360
  • 1
  • 11
  • 1
    Thank you so much for this answer! This is exactly what I needed. I didn't realize I had a method for manually sending a message outside of a callback. This worked like a charm! – Benjamin Peinhardt Aug 19 '22 at 04:39