This is unreleased documentation for Yew Next version.
For up-to-date documentation, see the latest version on docs.rs.

yew/html/component/
lifecycle.rs

1//! Component lifecycle module
2
3use std::any::Any;
4use std::rc::Rc;
5
6#[cfg(feature = "csr")]
7use web_sys::Element;
8
9use super::scope::{AnyScope, Scope};
10use super::BaseComponent;
11#[cfg(feature = "hydration")]
12use crate::dom_bundle::Fragment;
13#[cfg(feature = "csr")]
14use crate::dom_bundle::{BSubtree, Bundle, DomSlot, DynamicDomSlot};
15#[cfg(feature = "hydration")]
16use crate::html::RenderMode;
17use crate::html::{Html, RenderError};
18use crate::scheduler::{self, Runnable, Shared};
19use crate::suspense::{BaseSuspense, Suspension};
20use crate::{Callback, Context, HtmlResult};
21
22pub(crate) enum ComponentRenderState {
23    #[cfg(feature = "csr")]
24    Render {
25        bundle: Bundle,
26        root: BSubtree,
27        parent: Element,
28        /// The dom position in front of the next sibling
29        sibling_slot: DynamicDomSlot,
30        /// The dom position in front of this component. Adjusted whenever this component
31        /// re-renders.
32        own_slot: DynamicDomSlot,
33    },
34    #[cfg(feature = "hydration")]
35    Hydration {
36        fragment: Fragment,
37        root: BSubtree,
38        parent: Element,
39        sibling_slot: DynamicDomSlot,
40        own_slot: DynamicDomSlot,
41    },
42    #[cfg(feature = "ssr")]
43    Ssr {
44        sender: Option<crate::platform::pinned::oneshot::Sender<Html>>,
45    },
46}
47
48impl std::fmt::Debug for ComponentRenderState {
49    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50        match self {
51            #[cfg(feature = "csr")]
52            Self::Render {
53                ref bundle,
54                root,
55                ref parent,
56                ref sibling_slot,
57                ref own_slot,
58            } => f
59                .debug_struct("ComponentRenderState::Render")
60                .field("bundle", bundle)
61                .field("root", root)
62                .field("parent", parent)
63                .field("sibling_slot", sibling_slot)
64                .field("own_slot", own_slot)
65                .finish(),
66
67            #[cfg(feature = "hydration")]
68            Self::Hydration {
69                ref fragment,
70                ref parent,
71                ref sibling_slot,
72                ref own_slot,
73                ref root,
74            } => f
75                .debug_struct("ComponentRenderState::Hydration")
76                .field("fragment", fragment)
77                .field("root", root)
78                .field("parent", parent)
79                .field("sibling_slot", sibling_slot)
80                .field("own_slot", own_slot)
81                .finish(),
82
83            #[cfg(feature = "ssr")]
84            Self::Ssr { ref sender } => {
85                let sender_repr = match sender {
86                    Some(_) => "Some(_)",
87                    None => "None",
88                };
89
90                f.debug_struct("ComponentRenderState::Ssr")
91                    .field("sender", &sender_repr)
92                    .finish()
93            }
94        }
95    }
96}
97
98#[cfg(feature = "csr")]
99impl ComponentRenderState {
100    pub(crate) fn shift(&mut self, next_parent: Element, next_slot: DomSlot) {
101        match self {
102            #[cfg(feature = "csr")]
103            Self::Render {
104                bundle,
105                parent,
106                sibling_slot,
107                ..
108            } => {
109                bundle.shift(&next_parent, next_slot.clone());
110
111                *parent = next_parent;
112                sibling_slot.reassign(next_slot);
113            }
114            #[cfg(feature = "hydration")]
115            Self::Hydration {
116                fragment,
117                parent,
118                sibling_slot,
119                ..
120            } => {
121                fragment.shift(&next_parent, next_slot.clone());
122
123                *parent = next_parent;
124                sibling_slot.reassign(next_slot);
125            }
126
127            #[cfg(feature = "ssr")]
128            Self::Ssr { .. } => {
129                #[cfg(debug_assertions)]
130                panic!("shifting is not possible during SSR");
131            }
132        }
133    }
134}
135
136struct CompStateInner<COMP>
137where
138    COMP: BaseComponent,
139{
140    pub(crate) component: COMP,
141    pub(crate) context: Context<COMP>,
142}
143
144/// A trait to provide common,
145/// generic free behaviour across all components to reduce code size.
146///
147/// Mostly a thin wrapper that passes the context to a component's lifecycle
148/// methods.
149pub(crate) trait Stateful {
150    fn view(&self) -> HtmlResult;
151    #[cfg(feature = "csr")]
152    fn rendered(&mut self, first_render: bool);
153    fn destroy(&mut self);
154
155    fn any_scope(&self) -> AnyScope;
156
157    fn flush_messages(&mut self) -> bool;
158    #[cfg(feature = "csr")]
159    fn props_changed(&mut self, props: Rc<dyn Any>) -> bool;
160
161    fn as_any(&self) -> &dyn Any;
162
163    #[cfg(feature = "hydration")]
164    fn creation_mode(&self) -> RenderMode;
165}
166
167impl<COMP> Stateful for CompStateInner<COMP>
168where
169    COMP: BaseComponent,
170{
171    fn view(&self) -> HtmlResult {
172        self.component.view(&self.context)
173    }
174
175    #[cfg(feature = "csr")]
176    fn rendered(&mut self, first_render: bool) {
177        self.component.rendered(&self.context, first_render)
178    }
179
180    fn destroy(&mut self) {
181        self.component.destroy(&self.context);
182    }
183
184    fn any_scope(&self) -> AnyScope {
185        self.context.link().clone().into()
186    }
187
188    #[cfg(feature = "hydration")]
189    fn creation_mode(&self) -> RenderMode {
190        self.context.creation_mode()
191    }
192
193    fn flush_messages(&mut self) -> bool {
194        self.context
195            .link()
196            .pending_messages
197            .drain()
198            .into_iter()
199            .fold(false, |acc, msg| {
200                self.component.update(&self.context, msg) || acc
201            })
202    }
203
204    #[cfg(feature = "csr")]
205    fn props_changed(&mut self, props: Rc<dyn Any>) -> bool {
206        let props = match Rc::downcast::<COMP::Properties>(props) {
207            Ok(m) => m,
208            _ => return false,
209        };
210
211        if self.context.props != props {
212            let old_props = std::mem::replace(&mut self.context.props, props);
213            self.component.changed(&self.context, &old_props)
214        } else {
215            false
216        }
217    }
218
219    fn as_any(&self) -> &dyn Any {
220        self
221    }
222}
223
224pub(crate) struct ComponentState {
225    pub(super) inner: Box<dyn Stateful>,
226
227    pub(super) render_state: ComponentRenderState,
228
229    #[cfg(feature = "csr")]
230    has_rendered: bool,
231    #[cfg(feature = "hydration")]
232    pending_props: Option<Rc<dyn Any>>,
233
234    suspension: Option<Suspension>,
235
236    pub(crate) comp_id: usize,
237}
238
239impl ComponentState {
240    #[tracing::instrument(
241        level = tracing::Level::DEBUG,
242        name = "create",
243        skip_all,
244        fields(component.id = scope.id),
245    )]
246    fn new<COMP: BaseComponent>(
247        initial_render_state: ComponentRenderState,
248        scope: Scope<COMP>,
249        props: Rc<COMP::Properties>,
250        #[cfg(feature = "hydration")] prepared_state: Option<String>,
251    ) -> Self {
252        let comp_id = scope.id;
253        #[cfg(feature = "hydration")]
254        let creation_mode = {
255            match initial_render_state {
256                ComponentRenderState::Render { .. } => RenderMode::Render,
257                ComponentRenderState::Hydration { .. } => RenderMode::Hydration,
258                #[cfg(feature = "ssr")]
259                ComponentRenderState::Ssr { .. } => RenderMode::Ssr,
260            }
261        };
262
263        let context = Context {
264            scope,
265            props,
266            #[cfg(feature = "hydration")]
267            creation_mode,
268            #[cfg(feature = "hydration")]
269            prepared_state,
270        };
271
272        let inner = Box::new(CompStateInner {
273            component: COMP::create(&context),
274            context,
275        });
276
277        Self {
278            inner,
279            render_state: initial_render_state,
280            suspension: None,
281
282            #[cfg(feature = "csr")]
283            has_rendered: false,
284            #[cfg(feature = "hydration")]
285            pending_props: None,
286
287            comp_id,
288        }
289    }
290
291    pub(crate) fn downcast_comp_ref<COMP>(&self) -> Option<&COMP>
292    where
293        COMP: BaseComponent + 'static,
294    {
295        self.inner
296            .as_any()
297            .downcast_ref::<CompStateInner<COMP>>()
298            .map(|m| &m.component)
299    }
300
301    fn resume_existing_suspension(&mut self) {
302        if let Some(m) = self.suspension.take() {
303            let comp_scope = self.inner.any_scope();
304
305            let suspense_scope = comp_scope.find_parent_scope::<BaseSuspense>().unwrap();
306            BaseSuspense::resume(&suspense_scope, m);
307        }
308    }
309}
310
311pub(crate) struct CreateRunner<COMP: BaseComponent> {
312    pub initial_render_state: ComponentRenderState,
313    pub props: Rc<COMP::Properties>,
314    pub scope: Scope<COMP>,
315    #[cfg(feature = "hydration")]
316    pub prepared_state: Option<String>,
317}
318
319impl<COMP: BaseComponent> Runnable for CreateRunner<COMP> {
320    fn run(self: Box<Self>) {
321        let mut current_state = self.scope.state.borrow_mut();
322        if current_state.is_none() {
323            *current_state = Some(ComponentState::new(
324                self.initial_render_state,
325                self.scope.clone(),
326                self.props,
327                #[cfg(feature = "hydration")]
328                self.prepared_state,
329            ));
330        }
331    }
332}
333
334pub(crate) struct UpdateRunner {
335    pub state: Shared<Option<ComponentState>>,
336}
337
338impl ComponentState {
339    #[tracing::instrument(
340        level = tracing::Level::DEBUG,
341        skip(self),
342        fields(component.id = self.comp_id)
343    )]
344    fn update(&mut self) -> bool {
345        let schedule_render = self.inner.flush_messages();
346        tracing::trace!(schedule_render);
347        schedule_render
348    }
349}
350
351impl Runnable for UpdateRunner {
352    fn run(self: Box<Self>) {
353        if let Some(state) = self.state.borrow_mut().as_mut() {
354            let schedule_render = state.update();
355
356            if schedule_render {
357                scheduler::push_component_render(
358                    state.comp_id,
359                    Box::new(RenderRunner {
360                        state: self.state.clone(),
361                    }),
362                );
363                // Only run from the scheduler, so no need to call `scheduler::start()`
364            }
365        }
366    }
367}
368
369pub(crate) struct DestroyRunner {
370    pub state: Shared<Option<ComponentState>>,
371    pub parent_to_detach: bool,
372}
373
374impl ComponentState {
375    #[tracing::instrument(
376        level = tracing::Level::DEBUG,
377        skip(self),
378        fields(component.id = self.comp_id)
379    )]
380    fn destroy(mut self, parent_to_detach: bool) {
381        self.inner.destroy();
382        self.resume_existing_suspension();
383
384        match self.render_state {
385            #[cfg(feature = "csr")]
386            ComponentRenderState::Render {
387                bundle,
388                ref parent,
389                ref root,
390                ..
391            } => {
392                bundle.detach(root, parent, parent_to_detach);
393            }
394            // We need to detach the hydrate fragment if the component is not hydrated.
395            #[cfg(feature = "hydration")]
396            ComponentRenderState::Hydration {
397                ref root,
398                fragment,
399                ref parent,
400                ..
401            } => {
402                fragment.detach(root, parent, parent_to_detach);
403            }
404
405            #[cfg(feature = "ssr")]
406            ComponentRenderState::Ssr { .. } => {
407                let _ = parent_to_detach;
408            }
409        }
410    }
411}
412
413impl Runnable for DestroyRunner {
414    fn run(self: Box<Self>) {
415        if let Some(state) = self.state.borrow_mut().take() {
416            state.destroy(self.parent_to_detach);
417        }
418    }
419}
420
421pub(crate) struct RenderRunner {
422    pub state: Shared<Option<ComponentState>>,
423}
424
425impl ComponentState {
426    #[tracing::instrument(
427        level = tracing::Level::DEBUG,
428        skip_all,
429        fields(component.id = self.comp_id)
430    )]
431    fn render(&mut self, shared_state: &Shared<Option<ComponentState>>) {
432        let view = self.inner.view();
433        tracing::trace!(?view, "render result");
434        match view {
435            Ok(vnode) => self.commit_render(shared_state, vnode),
436            Err(RenderError::Suspended(susp)) => self.suspend(shared_state, susp),
437        };
438    }
439
440    fn suspend(&mut self, shared_state: &Shared<Option<ComponentState>>, suspension: Suspension) {
441        // Currently suspended, we re-use previous root node and send
442        // suspension to parent element.
443
444        if suspension.resumed() {
445            // schedule a render immediately if suspension is resumed.
446            scheduler::push_component_render(
447                self.comp_id,
448                Box::new(RenderRunner {
449                    state: shared_state.clone(),
450                }),
451            );
452        } else {
453            // We schedule a render after current suspension is resumed.
454            let comp_scope = self.inner.any_scope();
455
456            let suspense_scope = comp_scope
457                .find_parent_scope::<BaseSuspense>()
458                .expect("To suspend rendering, a <Suspense /> component is required.");
459
460            let comp_id = self.comp_id;
461            let shared_state = shared_state.clone();
462            suspension.listen(Callback::from(move |_| {
463                scheduler::push_component_render(
464                    comp_id,
465                    Box::new(RenderRunner {
466                        state: shared_state.clone(),
467                    }),
468                );
469                scheduler::start();
470            }));
471
472            if let Some(ref last_suspension) = self.suspension {
473                if &suspension != last_suspension {
474                    // We remove previous suspension from the suspense.
475                    BaseSuspense::resume(&suspense_scope, last_suspension.clone());
476                }
477            }
478            self.suspension = Some(suspension.clone());
479
480            BaseSuspense::suspend(&suspense_scope, suspension);
481        }
482    }
483
484    fn commit_render(&mut self, shared_state: &Shared<Option<ComponentState>>, new_root: Html) {
485        // Currently not suspended, we remove any previous suspension and update
486        // normally.
487        self.resume_existing_suspension();
488
489        match self.render_state {
490            #[cfg(feature = "csr")]
491            ComponentRenderState::Render {
492                ref mut bundle,
493                ref parent,
494                ref root,
495                ref sibling_slot,
496                ref mut own_slot,
497                ..
498            } => {
499                let scope = self.inner.any_scope();
500
501                let new_node_ref =
502                    bundle.reconcile(root, &scope, parent, sibling_slot.to_position(), new_root);
503                own_slot.reassign(new_node_ref);
504
505                let first_render = !self.has_rendered;
506                self.has_rendered = true;
507
508                scheduler::push_component_rendered(
509                    self.comp_id,
510                    Box::new(RenderedRunner {
511                        state: shared_state.clone(),
512                        first_render,
513                    }),
514                    first_render,
515                );
516            }
517
518            #[cfg(feature = "hydration")]
519            ComponentRenderState::Hydration {
520                ref mut fragment,
521                ref parent,
522                ref mut own_slot,
523                ref mut sibling_slot,
524                ref root,
525            } => {
526                // We schedule a "first" render to run immediately after hydration,
527                // to fix NodeRefs (first_node and slot).
528                scheduler::push_component_priority_render(
529                    self.comp_id,
530                    Box::new(RenderRunner {
531                        state: shared_state.clone(),
532                    }),
533                );
534
535                let scope = self.inner.any_scope();
536
537                // This first node is not guaranteed to be correct here.
538                // As it may be a comment node that is removed afterwards.
539                // but we link it anyways.
540                let bundle = Bundle::hydrate(root, &scope, parent, fragment, new_root);
541
542                // We trim all text nodes before checking as it's likely these are whitespaces.
543                fragment.trim_start_text_nodes();
544
545                assert!(fragment.is_empty(), "expected end of component, found node");
546
547                self.render_state = ComponentRenderState::Render {
548                    root: root.clone(),
549                    bundle,
550                    parent: parent.clone(),
551                    own_slot: std::mem::replace(own_slot, DynamicDomSlot::new_debug_trapped()),
552                    sibling_slot: std::mem::replace(
553                        sibling_slot,
554                        DynamicDomSlot::new_debug_trapped(),
555                    ),
556                };
557            }
558
559            #[cfg(feature = "ssr")]
560            ComponentRenderState::Ssr { ref mut sender } => {
561                let _ = shared_state;
562                if let Some(tx) = sender.take() {
563                    tx.send(new_root).unwrap();
564                }
565            }
566        };
567    }
568}
569
570impl Runnable for RenderRunner {
571    fn run(self: Box<Self>) {
572        let mut state = self.state.borrow_mut();
573        let state = match state.as_mut() {
574            None => return, // skip for components that have already been destroyed
575            Some(state) => state,
576        };
577
578        state.render(&self.state);
579    }
580}
581
582#[cfg(feature = "csr")]
583mod feat_csr {
584    use super::*;
585
586    pub(crate) struct PropsUpdateRunner {
587        pub state: Shared<Option<ComponentState>>,
588        pub props: Option<Rc<dyn Any>>,
589        pub next_sibling_slot: Option<DomSlot>,
590    }
591
592    impl ComponentState {
593        #[tracing::instrument(
594            level = tracing::Level::DEBUG,
595            skip(self),
596            fields(component.id = self.comp_id)
597        )]
598        fn changed(
599            &mut self,
600            props: Option<Rc<dyn Any>>,
601            next_sibling_slot: Option<DomSlot>,
602        ) -> bool {
603            if let Some(next_sibling_slot) = next_sibling_slot {
604                // When components are updated, their siblings were likely also updated
605                // We also need to shift the bundle so next sibling will be synced to child
606                // components.
607                match &mut self.render_state {
608                    #[cfg(feature = "csr")]
609                    ComponentRenderState::Render { sibling_slot, .. } => {
610                        sibling_slot.reassign(next_sibling_slot);
611                    }
612
613                    #[cfg(feature = "hydration")]
614                    ComponentRenderState::Hydration { sibling_slot, .. } => {
615                        sibling_slot.reassign(next_sibling_slot);
616                    }
617
618                    #[cfg(feature = "ssr")]
619                    ComponentRenderState::Ssr { .. } => {
620                        #[cfg(debug_assertions)]
621                        panic!("properties do not change during SSR");
622                    }
623                }
624            }
625
626            let should_render = |props: Option<Rc<dyn Any>>, state: &mut ComponentState| -> bool {
627                props.map(|m| state.inner.props_changed(m)).unwrap_or(false)
628            };
629
630            #[cfg(feature = "hydration")]
631            let should_render_hydration =
632                |props: Option<Rc<dyn Any>>, state: &mut ComponentState| -> bool {
633                    if let Some(props) = props.or_else(|| state.pending_props.take()) {
634                        match state.has_rendered {
635                            true => {
636                                state.pending_props = None;
637                                state.inner.props_changed(props)
638                            }
639                            false => {
640                                state.pending_props = Some(props);
641                                false
642                            }
643                        }
644                    } else {
645                        false
646                    }
647                };
648
649            // Only trigger changed if props were changed / next sibling has changed.
650            let schedule_render = {
651                #[cfg(feature = "hydration")]
652                {
653                    if self.inner.creation_mode() == RenderMode::Hydration {
654                        should_render_hydration(props, self)
655                    } else {
656                        should_render(props, self)
657                    }
658                }
659
660                #[cfg(not(feature = "hydration"))]
661                should_render(props, self)
662            };
663
664            tracing::trace!(
665                "props_update(has_rendered={} schedule_render={})",
666                self.has_rendered,
667                schedule_render
668            );
669            schedule_render
670        }
671    }
672
673    impl Runnable for PropsUpdateRunner {
674        fn run(self: Box<Self>) {
675            let Self {
676                next_sibling_slot,
677                props,
678                state: shared_state,
679            } = *self;
680
681            if let Some(state) = shared_state.borrow_mut().as_mut() {
682                let schedule_render = state.changed(props, next_sibling_slot);
683
684                if schedule_render {
685                    scheduler::push_component_render(
686                        state.comp_id,
687                        Box::new(RenderRunner {
688                            state: shared_state.clone(),
689                        }),
690                    );
691                    // Only run from the scheduler, so no need to call `scheduler::start()`
692                }
693            };
694        }
695    }
696
697    pub(crate) struct RenderedRunner {
698        pub state: Shared<Option<ComponentState>>,
699        pub first_render: bool,
700    }
701
702    impl ComponentState {
703        #[tracing::instrument(
704            level = tracing::Level::DEBUG,
705            skip(self),
706            fields(component.id = self.comp_id)
707        )]
708        fn rendered(&mut self, first_render: bool) -> bool {
709            if self.suspension.is_none() {
710                self.inner.rendered(first_render);
711            }
712
713            #[cfg(feature = "hydration")]
714            {
715                self.pending_props.is_some()
716            }
717            #[cfg(not(feature = "hydration"))]
718            {
719                false
720            }
721        }
722    }
723
724    impl Runnable for RenderedRunner {
725        fn run(self: Box<Self>) {
726            if let Some(state) = self.state.borrow_mut().as_mut() {
727                let has_pending_props = state.rendered(self.first_render);
728
729                if has_pending_props {
730                    scheduler::push_component_props_update(Box::new(PropsUpdateRunner {
731                        state: self.state.clone(),
732                        props: None,
733                        next_sibling_slot: None,
734                    }));
735                }
736            }
737        }
738    }
739}
740
741#[cfg(feature = "csr")]
742pub(super) use feat_csr::*;
743
744#[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))]
745#[cfg(test)]
746mod tests {
747    extern crate self as yew;
748
749    use std::cell::RefCell;
750    use std::ops::Deref;
751    use std::rc::Rc;
752
753    use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
754
755    use super::*;
756    use crate::dom_bundle::BSubtree;
757    use crate::html::*;
758    use crate::{html, Properties};
759
760    wasm_bindgen_test_configure!(run_in_browser);
761
762    #[derive(Clone, Properties, Default, PartialEq)]
763    struct ChildProps {
764        lifecycle: Rc<RefCell<Vec<String>>>,
765    }
766
767    struct Child {}
768
769    impl Component for Child {
770        type Message = ();
771        type Properties = ChildProps;
772
773        fn create(_ctx: &Context<Self>) -> Self {
774            Child {}
775        }
776
777        fn rendered(&mut self, ctx: &Context<Self>, _first_render: bool) {
778            ctx.props()
779                .lifecycle
780                .borrow_mut()
781                .push("child rendered".into());
782        }
783
784        fn update(&mut self, _ctx: &Context<Self>, _: Self::Message) -> bool {
785            false
786        }
787
788        fn changed(&mut self, _ctx: &Context<Self>, _old_props: &Self::Properties) -> bool {
789            false
790        }
791
792        fn view(&self, _ctx: &Context<Self>) -> Html {
793            html! {}
794        }
795    }
796
797    #[derive(Clone, Properties, Default, PartialEq)]
798    struct Props {
799        lifecycle: Rc<RefCell<Vec<String>>>,
800        #[allow(dead_code)]
801        #[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))]
802        create_message: Option<bool>,
803        update_message: RefCell<Option<bool>>,
804        view_message: RefCell<Option<bool>>,
805        rendered_message: RefCell<Option<bool>>,
806    }
807
808    struct Comp {
809        lifecycle: Rc<RefCell<Vec<String>>>,
810    }
811
812    impl Component for Comp {
813        type Message = bool;
814        type Properties = Props;
815
816        fn create(ctx: &Context<Self>) -> Self {
817            ctx.props().lifecycle.borrow_mut().push("create".into());
818            #[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))]
819            if let Some(msg) = ctx.props().create_message {
820                ctx.link().send_message(msg);
821            }
822            Comp {
823                lifecycle: Rc::clone(&ctx.props().lifecycle),
824            }
825        }
826
827        fn rendered(&mut self, ctx: &Context<Self>, first_render: bool) {
828            if let Some(msg) = ctx.props().rendered_message.borrow_mut().take() {
829                ctx.link().send_message(msg);
830            }
831            ctx.props()
832                .lifecycle
833                .borrow_mut()
834                .push(format!("rendered({})", first_render));
835        }
836
837        fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
838            if let Some(msg) = ctx.props().update_message.borrow_mut().take() {
839                ctx.link().send_message(msg);
840            }
841            ctx.props()
842                .lifecycle
843                .borrow_mut()
844                .push(format!("update({})", msg));
845            msg
846        }
847
848        fn changed(&mut self, ctx: &Context<Self>, _old_props: &Self::Properties) -> bool {
849            self.lifecycle = Rc::clone(&ctx.props().lifecycle);
850            self.lifecycle.borrow_mut().push("change".into());
851            false
852        }
853
854        fn view(&self, ctx: &Context<Self>) -> Html {
855            if let Some(msg) = ctx.props().view_message.borrow_mut().take() {
856                ctx.link().send_message(msg);
857            }
858            self.lifecycle.borrow_mut().push("view".into());
859            html! { <Child lifecycle={self.lifecycle.clone()} /> }
860        }
861    }
862
863    impl Drop for Comp {
864        fn drop(&mut self) {
865            self.lifecycle.borrow_mut().push("drop".into());
866        }
867    }
868
869    fn test_lifecycle(props: Props, expected: &[&str]) {
870        let document = gloo::utils::document();
871        let scope = Scope::<Comp>::new(None);
872        let parent = document.create_element("div").unwrap();
873        let root = BSubtree::create_root(&parent);
874
875        let lifecycle = props.lifecycle.clone();
876
877        lifecycle.borrow_mut().clear();
878        scope.mount_in_place(
879            root,
880            parent,
881            DomSlot::at_end(),
882            DynamicDomSlot::new_debug_trapped(),
883            Rc::new(props),
884        );
885        crate::scheduler::start_now();
886
887        assert_eq!(&lifecycle.borrow_mut().deref()[..], expected);
888    }
889
890    #[test]
891    fn lifecycle_tests() {
892        let lifecycle: Rc<RefCell<Vec<String>>> = Rc::default();
893
894        test_lifecycle(
895            Props {
896                lifecycle: lifecycle.clone(),
897                ..Props::default()
898            },
899            &["create", "view", "child rendered", "rendered(true)"],
900        );
901
902        test_lifecycle(
903            Props {
904                lifecycle: lifecycle.clone(),
905                #[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))]
906                create_message: Some(false),
907                ..Props::default()
908            },
909            &[
910                "create",
911                "view",
912                "child rendered",
913                "rendered(true)",
914                "update(false)",
915            ],
916        );
917
918        test_lifecycle(
919            Props {
920                lifecycle: lifecycle.clone(),
921                view_message: RefCell::new(Some(true)),
922                ..Props::default()
923            },
924            &[
925                "create",
926                "view",
927                "child rendered",
928                "rendered(true)",
929                "update(true)",
930                "view",
931                "rendered(false)",
932            ],
933        );
934
935        test_lifecycle(
936            Props {
937                lifecycle: lifecycle.clone(),
938                view_message: RefCell::new(Some(false)),
939                ..Props::default()
940            },
941            &[
942                "create",
943                "view",
944                "child rendered",
945                "rendered(true)",
946                "update(false)",
947            ],
948        );
949
950        test_lifecycle(
951            Props {
952                lifecycle: lifecycle.clone(),
953                rendered_message: RefCell::new(Some(false)),
954                ..Props::default()
955            },
956            &[
957                "create",
958                "view",
959                "child rendered",
960                "rendered(true)",
961                "update(false)",
962            ],
963        );
964
965        test_lifecycle(
966            Props {
967                lifecycle: lifecycle.clone(),
968                rendered_message: RefCell::new(Some(true)),
969                ..Props::default()
970            },
971            &[
972                "create",
973                "view",
974                "child rendered",
975                "rendered(true)",
976                "update(true)",
977                "view",
978                "rendered(false)",
979            ],
980        );
981
982        // This also tests render deduplication after the first render
983        test_lifecycle(
984            Props {
985                lifecycle,
986                #[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))]
987                create_message: Some(true),
988                update_message: RefCell::new(Some(true)),
989                ..Props::default()
990            },
991            &[
992                "create",
993                "view",
994                "child rendered",
995                "rendered(true)",
996                "update(true)",
997                "update(true)",
998                "view",
999                "rendered(false)",
1000            ],
1001        );
1002    }
1003}