suspense.rs 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374
  1. use dioxus::prelude::*;
  2. use std::future::poll_fn;
  3. use std::task::Poll;
  4. async fn poll_three_times() {
  5. // Poll each task 3 times
  6. let mut count = 0;
  7. poll_fn(|cx| {
  8. println!("polling... {}", count);
  9. if count < 3 {
  10. count += 1;
  11. cx.waker().wake_by_ref();
  12. Poll::Pending
  13. } else {
  14. Poll::Ready(())
  15. }
  16. })
  17. .await;
  18. }
  19. #[test]
  20. fn suspense_resolves() {
  21. // wait just a moment, not enough time for the boundary to resolve
  22. tokio::runtime::Builder::new_current_thread()
  23. .build()
  24. .unwrap()
  25. .block_on(async {
  26. let mut dom = VirtualDom::new(app);
  27. dom.rebuild(&mut dioxus_core::NoOpMutations);
  28. dom.wait_for_suspense().await;
  29. let out = dioxus_ssr::render(&dom);
  30. assert_eq!(out, "<div>Waiting for... child</div>");
  31. });
  32. }
  33. fn app() -> Element {
  34. rsx!(
  35. div {
  36. "Waiting for... "
  37. SuspenseBoundary {
  38. fallback: |_| rsx! { "fallback" },
  39. suspended_child {}
  40. }
  41. }
  42. )
  43. }
  44. fn suspended_child() -> Element {
  45. let mut val = use_signal(|| 0);
  46. // Tasks that are not suspended should never be polled
  47. spawn(async move {
  48. panic!("Non-suspended task was polled");
  49. });
  50. // Memos should still work like normal
  51. let memo = use_memo(move || val * 2);
  52. assert_eq!(memo, val * 2);
  53. if val() < 3 {
  54. let task = spawn(async move {
  55. poll_three_times().await;
  56. println!("waiting... {}", val);
  57. val += 1;
  58. });
  59. suspend(task)?;
  60. }
  61. rsx!("child")
  62. }
  63. /// When switching from a suspense fallback to the real child, the state of that component must be kept
  64. #[test]
  65. fn suspense_keeps_state() {
  66. tokio::runtime::Builder::new_current_thread()
  67. .enable_time()
  68. .build()
  69. .unwrap()
  70. .block_on(async {
  71. let mut dom = VirtualDom::new(app);
  72. dom.rebuild(&mut dioxus_core::NoOpMutations);
  73. dom.render_suspense_immediate().await;
  74. let out = dioxus_ssr::render(&dom);
  75. assert_eq!(out, "fallback");
  76. dom.wait_for_suspense().await;
  77. let out = dioxus_ssr::render(&dom);
  78. assert_eq!(out, "<div>child with future resolved</div>");
  79. });
  80. fn app() -> Element {
  81. rsx! {
  82. SuspenseBoundary {
  83. fallback: |_| rsx! { "fallback" },
  84. Child {}
  85. }
  86. }
  87. }
  88. #[component]
  89. fn Child() -> Element {
  90. let mut future_resolved = use_signal(|| false);
  91. let task = use_hook(|| {
  92. spawn(async move {
  93. tokio::time::sleep(std::time::Duration::from_millis(100)).await;
  94. future_resolved.set(true);
  95. })
  96. });
  97. if !future_resolved() {
  98. suspend(task)?;
  99. }
  100. println!("future resolved: {future_resolved:?}");
  101. if future_resolved() {
  102. rsx! {
  103. div { "child with future resolved" }
  104. }
  105. } else {
  106. rsx! {
  107. div { "this should never be rendered" }
  108. }
  109. }
  110. }
  111. }
  112. /// spawn doesn't run in suspense
  113. #[test]
  114. fn suspense_does_not_poll_spawn() {
  115. tokio::runtime::Builder::new_current_thread()
  116. .enable_time()
  117. .build()
  118. .unwrap()
  119. .block_on(async {
  120. let mut dom = VirtualDom::new(app);
  121. dom.rebuild(&mut dioxus_core::NoOpMutations);
  122. dom.wait_for_suspense().await;
  123. let out = dioxus_ssr::render(&dom);
  124. assert_eq!(out, "<div>child with future resolved</div>");
  125. });
  126. fn app() -> Element {
  127. rsx! {
  128. SuspenseBoundary {
  129. fallback: |_| rsx! { "fallback" },
  130. Child {}
  131. }
  132. }
  133. }
  134. #[component]
  135. fn Child() -> Element {
  136. let mut future_resolved = use_signal(|| false);
  137. // futures that are spawned, but not suspended should never be polled
  138. use_hook(|| {
  139. spawn(async move {
  140. panic!("Non-suspended task was polled");
  141. });
  142. });
  143. let task = use_hook(|| {
  144. spawn(async move {
  145. tokio::time::sleep(std::time::Duration::from_millis(100)).await;
  146. future_resolved.set(true);
  147. })
  148. });
  149. if !future_resolved() {
  150. suspend(task)?;
  151. }
  152. rsx! {
  153. div { "child with future resolved" }
  154. }
  155. }
  156. }
  157. /// suspended nodes are not mounted, so they should not run effects
  158. #[test]
  159. fn suspended_nodes_dont_trigger_effects() {
  160. tokio::runtime::Builder::new_current_thread()
  161. .enable_time()
  162. .build()
  163. .unwrap()
  164. .block_on(async {
  165. let mut dom = VirtualDom::new(app);
  166. dom.rebuild(&mut dioxus_core::NoOpMutations);
  167. let work = async move {
  168. loop {
  169. dom.wait_for_work().await;
  170. dom.render_immediate(&mut dioxus_core::NoOpMutations);
  171. }
  172. };
  173. tokio::select! {
  174. _ = work => {},
  175. _ = tokio::time::sleep(std::time::Duration::from_millis(100)) => {}
  176. }
  177. });
  178. fn app() -> Element {
  179. rsx! {
  180. SuspenseBoundary {
  181. fallback: |_| rsx! { "fallback" },
  182. Child {}
  183. }
  184. }
  185. }
  186. #[component]
  187. fn RerendersFrequently() -> Element {
  188. let mut count = use_signal(|| 0);
  189. use_future(move || async move {
  190. for _ in 0..100 {
  191. tokio::time::sleep(std::time::Duration::from_millis(10)).await;
  192. count.set(count() + 1);
  193. }
  194. });
  195. rsx! {
  196. div { "rerenders frequently" }
  197. }
  198. }
  199. #[component]
  200. fn Child() -> Element {
  201. let mut future_resolved = use_signal(|| false);
  202. use_effect(|| panic!("effects should not run during suspense"));
  203. let task = use_hook(|| {
  204. spawn(async move {
  205. tokio::time::sleep(std::time::Duration::from_millis(500)).await;
  206. future_resolved.set(true);
  207. })
  208. });
  209. if !future_resolved() {
  210. suspend(task)?;
  211. }
  212. rsx! {
  213. div { "child with future resolved" }
  214. }
  215. }
  216. }
  217. /// Make sure we keep any state of components when we switch from a resolved future to a suspended future
  218. #[test]
  219. fn resolved_to_suspended() {
  220. tracing_subscriber::fmt::SubscriberBuilder::default()
  221. .with_max_level(tracing::Level::INFO)
  222. .init();
  223. static SUSPENDED: GlobalSignal<bool> = Signal::global(|| false);
  224. tokio::runtime::Builder::new_current_thread()
  225. .enable_time()
  226. .build()
  227. .unwrap()
  228. .block_on(async {
  229. let mut dom = VirtualDom::new(app);
  230. dom.rebuild(&mut dioxus_core::NoOpMutations);
  231. let out = dioxus_ssr::render(&dom);
  232. assert_eq!(out, "rendered 1 times");
  233. dom.in_runtime(|| ScopeId::APP.in_runtime(|| *SUSPENDED.write() = true));
  234. dom.render_suspense_immediate().await;
  235. let out = dioxus_ssr::render(&dom);
  236. assert_eq!(out, "fallback");
  237. dom.wait_for_suspense().await;
  238. let out = dioxus_ssr::render(&dom);
  239. assert_eq!(out, "rendered 3 times");
  240. });
  241. fn app() -> Element {
  242. rsx! {
  243. SuspenseBoundary {
  244. fallback: |_| rsx! { "fallback" },
  245. Child {}
  246. }
  247. }
  248. }
  249. #[component]
  250. fn Child() -> Element {
  251. let mut render_count = use_signal(|| 0);
  252. render_count += 1;
  253. let mut task = use_hook(|| CopyValue::new(None));
  254. tracing::info!("render_count: {}", render_count.peek());
  255. if SUSPENDED() {
  256. if task().is_none() {
  257. task.set(Some(spawn(async move {
  258. tokio::time::sleep(std::time::Duration::from_millis(100)).await;
  259. tracing::info!("task finished");
  260. *SUSPENDED.write() = false;
  261. })));
  262. }
  263. suspend(task().unwrap())?;
  264. }
  265. rsx! {
  266. "rendered {render_count.peek()} times"
  267. }
  268. }
  269. }
  270. /// Make sure suspense tells the renderer that a suspense boundary was resolved
  271. #[test]
  272. fn suspense_tracks_resolved() {
  273. tokio::runtime::Builder::new_current_thread()
  274. .enable_time()
  275. .build()
  276. .unwrap()
  277. .block_on(async {
  278. let mut dom = VirtualDom::new(app);
  279. dom.rebuild(&mut dioxus_core::NoOpMutations);
  280. dom.render_suspense_immediate().await;
  281. dom.wait_for_suspense_work().await;
  282. assert_eq!(
  283. dom.render_suspense_immediate().await,
  284. vec![ScopeId(ScopeId::APP.0 + 1)]
  285. );
  286. });
  287. fn app() -> Element {
  288. rsx! {
  289. SuspenseBoundary {
  290. fallback: |_| rsx! { "fallback" },
  291. Child {}
  292. }
  293. }
  294. }
  295. #[component]
  296. fn Child() -> Element {
  297. let mut resolved = use_signal(|| false);
  298. let task = use_hook(|| {
  299. spawn(async move {
  300. tokio::time::sleep(std::time::Duration::from_millis(100)).await;
  301. tracing::info!("task finished");
  302. resolved.set(true);
  303. })
  304. });
  305. if resolved() {
  306. println!("suspense is resolved");
  307. } else {
  308. println!("suspense is not resolved");
  309. suspend(task)?;
  310. }
  311. rsx! {
  312. "child"
  313. }
  314. }
  315. }