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