1use std::future::Future;
4use std::num::NonZeroU32;
5
6use sycamore_futures::{
7 create_detatched_suspense_scope, create_suspense_scope, create_suspense_task,
8};
9use sycamore_macro::{component, Props};
10
11use crate::*;
12
13#[derive(Props)]
15pub struct SuspenseProps {
16 #[prop(default = Box::new(|| view! {}), setter(transform = |f: impl Fn() -> View + 'static| Box::new(f) as Box<dyn Fn() -> View>))]
18 fallback: Box<dyn Fn() -> View>,
19 children: Children,
20 #[prop(default = Box::new(|_| {}), setter(transform = |f: impl FnMut(bool) + 'static| Box::new(f) as Box<dyn FnMut(bool)>))]
24 set_is_loading: Box<dyn FnMut(bool) + 'static>,
25}
26
27#[component]
56pub fn Suspense(props: SuspenseProps) -> View {
57 let SuspenseProps {
58 fallback,
59 children,
60 mut set_is_loading,
61 } = props;
62
63 is_ssr! {
64 use futures::SinkExt;
65
66 let _ = &mut set_is_loading;
67
68 let mode = use_context::<SsrMode>();
69 match mode {
70 SsrMode::Sync => view! {
74 Show(when=true) {
75 (fallback())
76 }
77 Show(when=false) {}
78 },
79 SsrMode::Blocking | SsrMode::Streaming => {
85 let key = use_suspense_key();
88
89 let (mut view, suspense_scope) = create_suspense_scope(move || HydrationRegistry::in_suspense_scope(key, move || children.call()));
91 let state = use_context::<SuspenseState>();
92 sycamore_futures::spawn_local_scoped(async move {
95 suspense_scope.until_finished().await;
96 debug_assert!(!suspense_scope.sent.get());
97 create_effect(move || {
99 if !suspense_scope.sent.get() && suspense_scope.parent.as_ref().map_or(true, |parent| parent.get().sent.get()) {
100 let view = std::mem::take(&mut view);
101 let fragment = SuspenseFragment::new(key, view! { Show(when=true) { (view) } });
102 let mut state = state.clone();
103 sycamore_futures::spawn_local_scoped(async move {
104 let _ = state.sender.send(fragment).await;
105 });
106 suspense_scope.sent.set(true);
107 }
108 });
109 });
110
111 let start = view! { suspense-start(data-key=key.to_string()) };
113 let marker = View::from(move || SsrNode::SuspenseMarker { key: key.into() });
114 let end = view! { NoHydrate { suspense-end(data-key=key.to_string()) } };
115
116 if mode == SsrMode::Blocking {
117 view! { (start) (marker) (end) }
118 } else if mode == SsrMode::Streaming {
119 view! {
120 NoSsr {}
121 (start)
122 (marker)
123 NoHydrate(children=Children::new(fallback))
124 (end)
125 }
126 } else {
127 unreachable!()
128 }
129 }
130 }
131 }
132 is_not_ssr! {
133 let mode = if IS_HYDRATING.get() {
134 use_context::<SsrMode>()
135 } else {
136 SsrMode::Sync
137 };
138 match mode {
139 SsrMode::Sync => {
140 let (view, suspense_scope) = create_suspense_scope(move || children.call());
141 let is_loading = suspense_scope.is_loading();
142
143 create_effect(move || {
144 set_is_loading(is_loading.get());
145 });
146
147 view! {
148 Show(when=is_loading) {
149 (fallback())
150 }
151 Show(when=move || !is_loading.get()) {
152 (view)
153 }
154 }
155 }
156 SsrMode::Blocking | SsrMode::Streaming => {
157 let start = view! { suspense-start() };
166 let node = start.nodes[0].as_web_sys().unchecked_ref::<web_sys::Element>();
167 let key: NonZeroU32 = node.get_attribute("data-key").unwrap().parse().unwrap();
168
169 let (view, suspense_scope) = HydrationRegistry::in_suspense_scope(key, move || create_suspense_scope(move || children.call()));
170 let is_loading = suspense_scope.is_loading();
171
172 create_effect(move || {
173 set_is_loading(is_loading.get());
174 });
175
176 view! {
177 NoSsr {
178 Show(when=move || !IS_HYDRATING.get() && is_loading.get()) {
179 (fallback())
180 }
181 }
182 Show(when=move || !IS_HYDRATING.get() && !is_loading.get()) {
183 (view)
184 }
185 }
186 }
187 }
188 }
189}
190
191#[component]
194pub fn WrapAsync<F: Future<Output = View>>(f: impl FnOnce() -> F + 'static) -> View {
195 is_not_ssr! {
196 let mode = if IS_HYDRATING.get() {
197 use_context::<SsrMode>()
198 } else {
199 SsrMode::Sync
200 };
201 match mode {
202 SsrMode::Sync => {
203 let view = create_signal(View::default());
204 let ret = view! { ({
205 view.track();
206 view.update_silent(std::mem::take)
207 }) };
208 create_suspense_task(async move {
209 view.set(f().await);
210 });
211 ret
212 }
213 SsrMode::Blocking | SsrMode::Streaming => {
214 create_suspense_task(async move { f().await; });
216 view! {}
217 }
218 }
219 }
220 is_ssr! {
221 use std::sync::{Arc, Mutex};
222
223 let node = Arc::new(Mutex::new(View::default()));
224 create_suspense_task({
225 let node = Arc::clone(&node);
226 async move {
227 *node.lock().unwrap() = f().await;
228 }
229 });
230 View::from(move || SsrNode::Dynamic {
231 view: Arc::clone(&node),
232 })
233 }
234}
235
236#[component]
239pub fn Transition(props: SuspenseProps) -> View {
240 #[component(inline_props)]
243 fn TransitionInner(children: Children, set_is_loading: Box<dyn FnMut(bool)>) -> View {
244 let mut set_is_loading = set_is_loading;
246
247 let (children, scope) = create_detatched_suspense_scope(move || children.call());
250 create_suspense_task(scope.until_finished());
253
254 let is_loading = scope.is_loading();
255 create_effect(move || {
256 set_is_loading(is_loading.get());
257 });
258
259 view! {
260 (children)
261 }
262 }
263
264 view! {
265 Suspense(fallback=props.fallback, children=Children::new(move || {
266 view! { TransitionInner(children=props.children, set_is_loading=props.set_is_loading) }
267 }))
268 }
269}
270
271#[cfg_ssr]
272pub(crate) struct SuspenseFragment {
273 pub key: NonZeroU32,
274 pub view: View,
275}
276
277#[cfg_ssr]
278impl SuspenseFragment {
279 pub fn new(key: NonZeroU32, view: View) -> Self {
280 Self { key, view }
281 }
282}
283
284#[cfg_ssr]
286#[derive(Clone)]
287pub(crate) struct SuspenseState {
288 pub sender: futures::channel::mpsc::Sender<SuspenseFragment>,
289}
290
291#[derive(Debug, Clone, Copy)]
293struct SuspenseCounter {
294 next: Signal<NonZeroU32>,
295}
296
297impl SuspenseCounter {
298 fn new() -> Self {
299 Self {
300 next: create_signal(NonZeroU32::new(1).unwrap()),
301 }
302 }
303}
304
305pub fn use_suspense_key() -> NonZeroU32 {
307 let global_scope = use_global_scope();
308 let counter = global_scope.run_in(|| use_context_or_else(SuspenseCounter::new));
309
310 let next = counter.next.get();
311 counter.next.set(next.checked_add(1).unwrap());
312 next
313}