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}