Skip to main content
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::BaseComponent;
10use super::scope::{AnyScope, Scope};
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                bundle,
57                root,
58                parent,
59                sibling_slot,
60                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                fragment,
73                parent,
74                sibling_slot,
75                own_slot,
76                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 { 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        let mut changed = false;
196        for msg in self.context.link().pending_messages.drain() {
197            if self.component.update(&self.context, msg) {
198                changed = true;
199            }
200        }
201        changed
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    /// This deals with an edge case. Usually, we want to update props as fast as possible.
232    /// But, when a component hydrates and suspends, we want to continue using the initially given
233    /// props. This is prop updates are ignored during SSR, too.
234    #[cfg(feature = "hydration")]
235    pending_props: Option<Rc<dyn Any>>,
236
237    suspension: Option<Suspension>,
238
239    pub(crate) comp_id: usize,
240}
241
242impl ComponentState {
243    #[tracing::instrument(
244        level = tracing::Level::DEBUG,
245        name = "create",
246        skip_all,
247        fields(component.id = scope.id),
248    )]
249    fn new<COMP: BaseComponent>(
250        initial_render_state: ComponentRenderState,
251        scope: Scope<COMP>,
252        props: Rc<COMP::Properties>,
253        #[cfg(feature = "hydration")] prepared_state: Option<String>,
254    ) -> Self {
255        let comp_id = scope.id;
256        #[cfg(feature = "hydration")]
257        let creation_mode = {
258            match initial_render_state {
259                ComponentRenderState::Render { .. } => RenderMode::Render,
260                ComponentRenderState::Hydration { .. } => RenderMode::Hydration,
261                #[cfg(feature = "ssr")]
262                ComponentRenderState::Ssr { .. } => RenderMode::Ssr,
263            }
264        };
265
266        let context = Context {
267            scope,
268            props,
269            #[cfg(feature = "hydration")]
270            creation_mode,
271            #[cfg(feature = "hydration")]
272            prepared_state,
273        };
274
275        let inner = Box::new(CompStateInner {
276            component: COMP::create(&context),
277            context,
278        });
279
280        Self {
281            inner,
282            render_state: initial_render_state,
283            suspension: None,
284
285            #[cfg(feature = "csr")]
286            has_rendered: false,
287            #[cfg(feature = "hydration")]
288            pending_props: None,
289
290            comp_id,
291        }
292    }
293
294    pub(crate) fn downcast_comp_ref<COMP>(&self) -> Option<&COMP>
295    where
296        COMP: BaseComponent + 'static,
297    {
298        self.inner
299            .as_any()
300            .downcast_ref::<CompStateInner<COMP>>()
301            .map(|m| &m.component)
302    }
303
304    fn resume_existing_suspension(&mut self) {
305        if let Some(m) = self.suspension.take() {
306            let comp_scope = self.inner.any_scope();
307
308            let suspense_scope = comp_scope.find_parent_scope::<BaseSuspense>().unwrap();
309            BaseSuspense::resume(&suspense_scope, m);
310        }
311    }
312}
313
314pub(crate) struct CreateRunner<COMP: BaseComponent> {
315    pub initial_render_state: ComponentRenderState,
316    pub props: Rc<COMP::Properties>,
317    pub scope: Scope<COMP>,
318    #[cfg(feature = "hydration")]
319    pub prepared_state: Option<String>,
320}
321
322impl<COMP: BaseComponent> Runnable for CreateRunner<COMP> {
323    fn run(self: Box<Self>) {
324        let mut current_state = self.scope.state.borrow_mut();
325        if current_state.is_none() {
326            *current_state = Some(ComponentState::new(
327                self.initial_render_state,
328                self.scope.clone(),
329                self.props,
330                #[cfg(feature = "hydration")]
331                self.prepared_state,
332            ));
333        }
334    }
335}
336
337pub(crate) struct UpdateRunner {
338    pub state: Shared<Option<ComponentState>>,
339}
340
341impl ComponentState {
342    #[tracing::instrument(
343        level = tracing::Level::DEBUG,
344        skip(self),
345        fields(component.id = self.comp_id)
346    )]
347    fn update(&mut self) -> bool {
348        let schedule_render = self.inner.flush_messages();
349        tracing::trace!(schedule_render);
350        schedule_render
351    }
352}
353
354impl Runnable for UpdateRunner {
355    fn run(self: Box<Self>) {
356        if let Some(state) = self.state.borrow_mut().as_mut() {
357            let schedule_render = state.update();
358
359            if schedule_render {
360                scheduler::push_component_render(
361                    state.comp_id,
362                    Box::new(RenderRunner {
363                        state: self.state.clone(),
364                    }),
365                );
366                // Only run from the scheduler, so no need to call `scheduler::start()`
367            }
368        }
369    }
370}
371
372pub(crate) struct DestroyRunner {
373    pub state: Shared<Option<ComponentState>>,
374    pub parent_to_detach: bool,
375}
376
377impl ComponentState {
378    #[tracing::instrument(
379        level = tracing::Level::DEBUG,
380        skip(self),
381        fields(component.id = self.comp_id)
382    )]
383    fn destroy(mut self, parent_to_detach: bool) {
384        self.inner.destroy();
385        self.resume_existing_suspension();
386
387        match self.render_state {
388            #[cfg(feature = "csr")]
389            ComponentRenderState::Render {
390                bundle,
391                ref parent,
392                ref root,
393                ..
394            } => {
395                bundle.detach(root, parent, parent_to_detach);
396            }
397            // We need to detach the hydrate fragment if the component is not hydrated.
398            #[cfg(feature = "hydration")]
399            ComponentRenderState::Hydration {
400                ref root,
401                fragment,
402                ref parent,
403                ..
404            } => {
405                fragment.detach(root, parent, parent_to_detach);
406            }
407
408            #[cfg(feature = "ssr")]
409            ComponentRenderState::Ssr { .. } => {
410                let _ = parent_to_detach;
411            }
412        }
413    }
414}
415
416impl Runnable for DestroyRunner {
417    fn run(self: Box<Self>) {
418        if let Some(state) = self.state.borrow_mut().take() {
419            state.destroy(self.parent_to_detach);
420        }
421    }
422}
423
424pub(crate) struct RenderRunner {
425    pub state: Shared<Option<ComponentState>>,
426}
427
428impl ComponentState {
429    #[tracing::instrument(
430        level = tracing::Level::DEBUG,
431        skip_all,
432        fields(component.id = self.comp_id)
433    )]
434    fn render(&mut self, shared_state: &Shared<Option<ComponentState>>) {
435        let view = self.inner.view();
436        tracing::trace!(?view, "render result");
437        match view {
438            Ok(vnode) => self.commit_render(shared_state, vnode),
439            Err(RenderError::Suspended(susp)) => self.suspend(shared_state, susp),
440        };
441    }
442
443    fn suspend(&mut self, shared_state: &Shared<Option<ComponentState>>, suspension: Suspension) {
444        // Currently suspended, we re-use previous root node and send
445        // suspension to parent element.
446
447        if suspension.resumed() {
448            // schedule a render immediately if suspension is resumed.
449            scheduler::push_component_render(
450                self.comp_id,
451                Box::new(RenderRunner {
452                    state: shared_state.clone(),
453                }),
454            );
455        } else {
456            // We schedule a render after current suspension is resumed.
457            let comp_scope = self.inner.any_scope();
458
459            let suspense_scope = comp_scope
460                .find_parent_scope::<BaseSuspense>()
461                .expect("To suspend rendering, a <Suspense /> component is required.");
462
463            let comp_id = self.comp_id;
464            let shared_state = shared_state.clone();
465            suspension.listen(Callback::from(move |_| {
466                scheduler::push_component_render(
467                    comp_id,
468                    Box::new(RenderRunner {
469                        state: shared_state.clone(),
470                    }),
471                );
472                scheduler::start();
473            }));
474
475            if let Some(ref last_suspension) = self.suspension {
476                if &suspension != last_suspension {
477                    // We remove previous suspension from the suspense.
478                    BaseSuspense::resume(&suspense_scope, last_suspension.clone());
479                }
480            }
481            self.suspension = Some(suspension.clone());
482
483            BaseSuspense::suspend(&suspense_scope, suspension);
484        }
485    }
486
487    fn commit_render(&mut self, shared_state: &Shared<Option<ComponentState>>, new_vdom: Html) {
488        // Currently not suspended, we remove any previous suspension and update
489        // normally.
490        #[cfg(feature = "csr")]
491        let resuming_from_suspension = self.suspension.is_some();
492        self.resume_existing_suspension();
493
494        match self.render_state {
495            #[cfg(feature = "csr")]
496            ComponentRenderState::Render {
497                ref mut bundle,
498                ref parent,
499                ref root,
500                ref sibling_slot,
501                ref mut own_slot,
502                ..
503            } => {
504                let scope = self.inner.any_scope();
505
506                let new_node_ref =
507                    bundle.reconcile(root, &scope, parent, sibling_slot.to_position(), new_vdom);
508                own_slot.reassign(new_node_ref);
509
510                let first_render = !self.has_rendered;
511                self.has_rendered = true;
512
513                if resuming_from_suspension {
514                    // The DOM we just reconciled still lives in the ancestor
515                    // Suspense's detached parent. The Suspense must process
516                    // the Resume message and re-render to shift children into
517                    // the live tree before our effects observe the DOM.
518                    // Hand the pending `rendered` to the Suspense; it will
519                    // re-schedule it once it fully un-suspends.
520                    let pending = PendingRendered::new(shared_state.clone(), first_render);
521                    let suspense_scope = scope
522                        .find_parent_scope::<BaseSuspense>()
523                        .expect("a resuming component must have a Suspense ancestor");
524                    BaseSuspense::defer_rendered(&suspense_scope, self.comp_id, pending);
525                } else {
526                    scheduler::push_component_rendered(
527                        self.comp_id,
528                        Box::new(RenderedRunner {
529                            state: shared_state.clone(),
530                            first_render,
531                        }),
532                        first_render,
533                    );
534                }
535            }
536
537            #[cfg(feature = "hydration")]
538            ComponentRenderState::Hydration {
539                ref mut fragment,
540                ref parent,
541                ref mut own_slot,
542                ref mut sibling_slot,
543                ref root,
544            } => {
545                // We schedule a "first" render to run immediately after hydration.
546                // Most notably, only this render will trigger the "rendered" callback, hence we
547                // want to prioritize this.
548                scheduler::push_component_priority_render(
549                    self.comp_id,
550                    Box::new(RenderRunner {
551                        state: shared_state.clone(),
552                    }),
553                );
554
555                let scope = self.inner.any_scope();
556                let bundle = Bundle::hydrate(
557                    root,
558                    &scope,
559                    parent,
560                    fragment,
561                    new_vdom,
562                    &mut Some(own_slot.clone()),
563                );
564
565                // We trim all text nodes before checking as it's likely these are whitespaces.
566                fragment.trim_start_text_nodes();
567                assert!(fragment.is_empty(), "expected end of component, found node");
568
569                self.render_state = ComponentRenderState::Render {
570                    root: root.clone(),
571                    bundle,
572                    parent: parent.clone(),
573                    own_slot: own_slot.take(),
574                    sibling_slot: sibling_slot.take(),
575                };
576            }
577
578            #[cfg(feature = "ssr")]
579            ComponentRenderState::Ssr { ref mut sender } => {
580                let _ = shared_state;
581                if let Some(tx) = sender.take() {
582                    tx.send(new_vdom).unwrap();
583                }
584            }
585        };
586    }
587}
588
589impl Runnable for RenderRunner {
590    fn run(self: Box<Self>) {
591        let mut state = self.state.borrow_mut();
592        let state = match state.as_mut() {
593            None => return, // skip for components that have already been destroyed
594            Some(state) => state,
595        };
596
597        state.render(&self.state);
598    }
599}
600
601#[cfg(feature = "csr")]
602mod feat_csr {
603    use super::*;
604
605    pub(crate) struct PropsUpdateRunner {
606        pub state: Shared<Option<ComponentState>>,
607        pub props: Option<Rc<dyn Any>>,
608        pub next_sibling_slot: Option<DomSlot>,
609    }
610
611    impl ComponentState {
612        #[tracing::instrument(
613            level = tracing::Level::DEBUG,
614            skip(self),
615            fields(component.id = self.comp_id)
616        )]
617        fn changed(
618            &mut self,
619            props: Option<Rc<dyn Any>>,
620            next_sibling_slot: Option<DomSlot>,
621        ) -> bool {
622            if let Some(next_sibling_slot) = next_sibling_slot {
623                // When components are updated, their siblings were likely also updated
624                // We also need to shift the bundle so next sibling will be synced to child
625                // components.
626                match &mut self.render_state {
627                    #[cfg(feature = "csr")]
628                    ComponentRenderState::Render { sibling_slot, .. } => {
629                        sibling_slot.reassign(next_sibling_slot);
630                    }
631
632                    #[cfg(feature = "hydration")]
633                    ComponentRenderState::Hydration { sibling_slot, .. } => {
634                        sibling_slot.reassign(next_sibling_slot);
635                    }
636
637                    #[cfg(feature = "ssr")]
638                    ComponentRenderState::Ssr { .. } => {
639                        #[cfg(debug_assertions)]
640                        panic!("properties do not change during SSR");
641                    }
642                }
643            }
644
645            let should_render = |props: Option<Rc<dyn Any>>, state: &mut ComponentState| -> bool {
646                props.map(|m| state.inner.props_changed(m)).unwrap_or(false)
647            };
648
649            #[cfg(feature = "hydration")]
650            let should_render_hydration =
651                |props: Option<Rc<dyn Any>>, state: &mut ComponentState| -> bool {
652                    match props.or_else(|| state.pending_props.take()) {
653                        Some(props) => match state.has_rendered {
654                            true => {
655                                state.pending_props = None;
656                                state.inner.props_changed(props)
657                            }
658                            false => {
659                                state.pending_props = Some(props);
660                                false
661                            }
662                        },
663                        _ => false,
664                    }
665                };
666
667            // Only trigger changed if props were changed / next sibling has changed.
668            let schedule_render = {
669                #[cfg(feature = "hydration")]
670                {
671                    if self.inner.creation_mode() == RenderMode::Hydration {
672                        should_render_hydration(props, self)
673                    } else {
674                        should_render(props, self)
675                    }
676                }
677
678                #[cfg(not(feature = "hydration"))]
679                should_render(props, self)
680            };
681
682            tracing::trace!(
683                "props_update(has_rendered={} schedule_render={})",
684                self.has_rendered,
685                schedule_render
686            );
687            schedule_render
688        }
689    }
690
691    impl Runnable for PropsUpdateRunner {
692        fn run(self: Box<Self>) {
693            let Self {
694                next_sibling_slot,
695                props,
696                state: shared_state,
697            } = *self;
698
699            if let Some(state) = shared_state.borrow_mut().as_mut() {
700                let schedule_render = state.changed(props, next_sibling_slot);
701
702                if schedule_render {
703                    scheduler::push_component_render(
704                        state.comp_id,
705                        Box::new(RenderRunner {
706                            state: shared_state.clone(),
707                        }),
708                    );
709                    // Only run from the scheduler, so no need to call `scheduler::start()`
710                }
711            };
712        }
713    }
714
715    pub(crate) struct RenderedRunner {
716        pub state: Shared<Option<ComponentState>>,
717        pub first_render: bool,
718    }
719
720    /// A `rendered` lifecycle deferred by the ancestor `<Suspense>` so it fires
721    /// only after Suspense un-suspends and children's DOM has been shifted into
722    /// the live tree. See `BaseSuspense::defer_rendered`.
723    pub(crate) struct PendingRendered {
724        pub state: Shared<Option<ComponentState>>,
725        pub first_render: bool,
726    }
727
728    impl PendingRendered {
729        pub(crate) fn new(state: Shared<Option<ComponentState>>, first_render: bool) -> Self {
730            Self {
731                state,
732                first_render,
733            }
734        }
735
736        /// Absorb a later-committed pending rendered for the same component:
737        /// keep the latest state but preserve `first_render=true` if either
738        /// side carried it.
739        pub(crate) fn absorb(&mut self, later: Self) {
740            self.state = later.state;
741            self.first_render |= later.first_render;
742        }
743
744        /// Push this onto the scheduler's `rendered` queue.
745        pub(crate) fn schedule(self, comp_id: usize) {
746            let PendingRendered {
747                state,
748                first_render,
749            } = self;
750            scheduler::push_component_rendered(
751                comp_id,
752                Box::new(RenderedRunner {
753                    state,
754                    first_render,
755                }),
756                false,
757            );
758        }
759    }
760
761    impl ComponentState {
762        #[tracing::instrument(
763            level = tracing::Level::DEBUG,
764            skip(self),
765            fields(component.id = self.comp_id)
766        )]
767        fn rendered(&mut self, first_render: bool) -> bool {
768            if self.suspension.is_none() {
769                self.inner.rendered(first_render);
770            }
771
772            #[cfg(feature = "hydration")]
773            {
774                self.pending_props.is_some()
775            }
776            #[cfg(not(feature = "hydration"))]
777            {
778                false
779            }
780        }
781    }
782
783    impl Runnable for RenderedRunner {
784        fn run(self: Box<Self>) {
785            if let Some(state) = self.state.borrow_mut().as_mut() {
786                let has_pending_props = state.rendered(self.first_render);
787
788                if has_pending_props {
789                    scheduler::push_component_props_update(Box::new(PropsUpdateRunner {
790                        state: self.state.clone(),
791                        props: None,
792                        next_sibling_slot: None,
793                    }));
794                }
795            }
796        }
797    }
798}
799
800#[cfg(feature = "csr")]
801pub(crate) use feat_csr::*;
802
803#[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))]
804#[cfg(test)]
805mod tests {
806    extern crate self as yew;
807
808    use std::cell::RefCell;
809    use std::ops::Deref;
810    use std::rc::Rc;
811
812    use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
813
814    use super::*;
815    use crate::dom_bundle::BSubtree;
816    use crate::html::*;
817    use crate::{Properties, html};
818
819    wasm_bindgen_test_configure!(run_in_browser);
820
821    #[derive(Clone, Properties, Default, PartialEq)]
822    struct ChildProps {
823        lifecycle: Rc<RefCell<Vec<String>>>,
824    }
825
826    struct Child {}
827
828    impl Component for Child {
829        type Message = ();
830        type Properties = ChildProps;
831
832        fn create(_ctx: &Context<Self>) -> Self {
833            Child {}
834        }
835
836        fn rendered(&mut self, ctx: &Context<Self>, _first_render: bool) {
837            ctx.props()
838                .lifecycle
839                .borrow_mut()
840                .push("child rendered".into());
841        }
842
843        fn update(&mut self, _ctx: &Context<Self>, _: Self::Message) -> bool {
844            false
845        }
846
847        fn changed(&mut self, _ctx: &Context<Self>, _old_props: &Self::Properties) -> bool {
848            false
849        }
850
851        fn view(&self, _ctx: &Context<Self>) -> Html {
852            html! {}
853        }
854    }
855
856    #[derive(Clone, Properties, Default, PartialEq)]
857    struct Props {
858        lifecycle: Rc<RefCell<Vec<String>>>,
859        #[allow(dead_code)]
860        #[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))]
861        create_message: Option<bool>,
862        update_message: RefCell<Option<bool>>,
863        view_message: RefCell<Option<bool>>,
864        rendered_message: RefCell<Option<bool>>,
865    }
866
867    struct Comp {
868        lifecycle: Rc<RefCell<Vec<String>>>,
869    }
870
871    impl Component for Comp {
872        type Message = bool;
873        type Properties = Props;
874
875        fn create(ctx: &Context<Self>) -> Self {
876            ctx.props().lifecycle.borrow_mut().push("create".into());
877            #[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))]
878            if let Some(msg) = ctx.props().create_message {
879                ctx.link().send_message(msg);
880            }
881            Comp {
882                lifecycle: Rc::clone(&ctx.props().lifecycle),
883            }
884        }
885
886        fn rendered(&mut self, ctx: &Context<Self>, first_render: bool) {
887            if let Some(msg) = ctx.props().rendered_message.borrow_mut().take() {
888                ctx.link().send_message(msg);
889            }
890            ctx.props()
891                .lifecycle
892                .borrow_mut()
893                .push(format!("rendered({})", first_render));
894        }
895
896        fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
897            if let Some(msg) = ctx.props().update_message.borrow_mut().take() {
898                ctx.link().send_message(msg);
899            }
900            ctx.props()
901                .lifecycle
902                .borrow_mut()
903                .push(format!("update({})", msg));
904            msg
905        }
906
907        fn changed(&mut self, ctx: &Context<Self>, _old_props: &Self::Properties) -> bool {
908            self.lifecycle = Rc::clone(&ctx.props().lifecycle);
909            self.lifecycle.borrow_mut().push("change".into());
910            false
911        }
912
913        fn view(&self, ctx: &Context<Self>) -> Html {
914            if let Some(msg) = ctx.props().view_message.borrow_mut().take() {
915                ctx.link().send_message(msg);
916            }
917            self.lifecycle.borrow_mut().push("view".into());
918            html! { <Child lifecycle={self.lifecycle.clone()} /> }
919        }
920    }
921
922    impl Drop for Comp {
923        fn drop(&mut self) {
924            self.lifecycle.borrow_mut().push("drop".into());
925        }
926    }
927
928    fn test_lifecycle(props: Props, expected: &[&str]) {
929        let document = gloo::utils::document();
930        let scope = Scope::<Comp>::new(None);
931        let parent = document.create_element("div").unwrap();
932        let root = BSubtree::create_root(&parent);
933
934        let lifecycle = props.lifecycle.clone();
935
936        lifecycle.borrow_mut().clear();
937        let _ = scope.mount_in_place(root, parent, DomSlot::at_end(), Rc::new(props));
938        crate::scheduler::start_now();
939
940        assert_eq!(&lifecycle.borrow_mut().deref()[..], expected);
941    }
942
943    #[test]
944    fn lifecycle_tests() {
945        let lifecycle: Rc<RefCell<Vec<String>>> = Rc::default();
946
947        test_lifecycle(
948            Props {
949                lifecycle: lifecycle.clone(),
950                ..Props::default()
951            },
952            &["create", "view", "child rendered", "rendered(true)"],
953        );
954
955        test_lifecycle(
956            Props {
957                lifecycle: lifecycle.clone(),
958                #[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))]
959                create_message: Some(false),
960                ..Props::default()
961            },
962            &[
963                "create",
964                "view",
965                "child rendered",
966                "rendered(true)",
967                "update(false)",
968            ],
969        );
970
971        test_lifecycle(
972            Props {
973                lifecycle: lifecycle.clone(),
974                view_message: RefCell::new(Some(true)),
975                ..Props::default()
976            },
977            &[
978                "create",
979                "view",
980                "child rendered",
981                "rendered(true)",
982                "update(true)",
983                "view",
984                "rendered(false)",
985            ],
986        );
987
988        test_lifecycle(
989            Props {
990                lifecycle: lifecycle.clone(),
991                view_message: RefCell::new(Some(false)),
992                ..Props::default()
993            },
994            &[
995                "create",
996                "view",
997                "child rendered",
998                "rendered(true)",
999                "update(false)",
1000            ],
1001        );
1002
1003        test_lifecycle(
1004            Props {
1005                lifecycle: lifecycle.clone(),
1006                rendered_message: RefCell::new(Some(false)),
1007                ..Props::default()
1008            },
1009            &[
1010                "create",
1011                "view",
1012                "child rendered",
1013                "rendered(true)",
1014                "update(false)",
1015            ],
1016        );
1017
1018        test_lifecycle(
1019            Props {
1020                lifecycle: lifecycle.clone(),
1021                rendered_message: RefCell::new(Some(true)),
1022                ..Props::default()
1023            },
1024            &[
1025                "create",
1026                "view",
1027                "child rendered",
1028                "rendered(true)",
1029                "update(true)",
1030                "view",
1031                "rendered(false)",
1032            ],
1033        );
1034
1035        // This also tests render deduplication after the first render
1036        test_lifecycle(
1037            Props {
1038                lifecycle,
1039                #[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))]
1040                create_message: Some(true),
1041                update_message: RefCell::new(Some(true)),
1042                ..Props::default()
1043            },
1044            &[
1045                "create",
1046                "view",
1047                "child rendered",
1048                "rendered(true)",
1049                "update(true)",
1050                "update(true)",
1051                "view",
1052                "rendered(false)",
1053            ],
1054        );
1055    }
1056}