leptos/
transition.rs

1use crate::{
2    children::{TypedChildren, ViewFnOnce},
3    suspense_component::SuspenseBoundary,
4    IntoView,
5};
6use leptos_macro::component;
7use reactive_graph::{
8    computed::{suspense::SuspenseContext, ArcMemo},
9    effect::Effect,
10    owner::{provide_context, Owner},
11    signal::ArcRwSignal,
12    traits::{Get, Set, Track, With},
13    wrappers::write::SignalSetter,
14};
15use slotmap::{DefaultKey, SlotMap};
16use tachys::reactive_graph::OwnedView;
17
18/// If any [`Resource`](leptos_reactive::Resource) is read in the `children` of this
19/// component, it will show the `fallback` while they are loading. Once all are resolved,
20/// it will render the `children`.
21///
22/// Unlike [`Suspense`](crate::Suspense), this will not fall
23/// back to the `fallback` state if there are further changes after the initial load.
24///
25/// Note that the `children` will be rendered initially (in order to capture the fact that
26/// those resources are read under the suspense), so you cannot assume that resources read
27/// synchronously have
28/// `Some` value in `children`. However, you can read resources asynchronously by using
29/// [Suspend](crate::prelude::Suspend).
30///
31/// ```
32/// # use leptos::prelude::*;
33/// # if false { // don't run in doctests
34/// async fn fetch_cats(how_many: u32) -> Vec<String> { vec![] }
35///
36/// let (cat_count, set_cat_count) = signal::<u32>(1);
37///
38/// let cats = Resource::new(move || cat_count.get(), |count| fetch_cats(count));
39///
40/// view! {
41///   <div>
42///     <Transition fallback=move || view! { <p>"Loading (Suspense Fallback)..."</p> }>
43///       // you can access a resource synchronously
44///       {move || {
45///           cats.get().map(|data| {
46///             data
47///               .into_iter()
48///               .map(|src| {
49///                   view! {
50///                     <img src={src}/>
51///                   }
52///               })
53///               .collect_view()
54///           })
55///         }
56///       }
57///       // or you can use `Suspend` to read resources asynchronously
58///       {move || Suspend::new(async move {
59///         cats.await
60///               .into_iter()
61///               .map(|src| {
62///                   view! {
63///                     <img src={src}/>
64///                   }
65///               })
66///               .collect_view()
67///       })}
68///     </Transition>
69///   </div>
70/// }
71/// # ;}
72/// ```
73#[component]
74pub fn Transition<Chil>(
75    /// Will be displayed while resources are pending. By default this is the empty view.
76    #[prop(optional, into)]
77    fallback: ViewFnOnce,
78    /// A function that will be called when the component transitions into or out of
79    /// the `pending` state, with its argument indicating whether it is pending (`true`)
80    /// or not pending (`false`).
81    #[prop(optional, into)]
82    set_pending: Option<SignalSetter<bool>>,
83    children: TypedChildren<Chil>,
84) -> impl IntoView
85where
86    Chil: IntoView + Send + 'static,
87{
88    let owner = Owner::new();
89    owner.with(|| {
90        let (starts_local, id) = {
91            Owner::current_shared_context()
92                .map(|sc| {
93                    let id = sc.next_id();
94                    (sc.get_incomplete_chunk(&id), id)
95                })
96                .unwrap_or_else(|| (false, Default::default()))
97        };
98        let fallback = fallback.run();
99        let children = children.into_inner()();
100        let tasks = ArcRwSignal::new(SlotMap::<DefaultKey, ()>::new());
101        provide_context(SuspenseContext {
102            tasks: tasks.clone(),
103        });
104        let none_pending = ArcMemo::new(move |prev: Option<&bool>| {
105            tasks.track();
106            if prev.is_none() && starts_local {
107                false
108            } else {
109                tasks.with(SlotMap::is_empty)
110            }
111        });
112        if let Some(set_pending) = set_pending {
113            Effect::new_isomorphic({
114                let none_pending = none_pending.clone();
115                move |_| {
116                    set_pending.set(!none_pending.get());
117                }
118            });
119        }
120
121        OwnedView::new(SuspenseBoundary::<true, _, _> {
122            id,
123            none_pending,
124            fallback,
125            children,
126        })
127    })
128}