Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Wait for the suspense boundary above the router to resolve before sending the first streaming chunk #3891

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
31 changes: 30 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ members = [
"packages/document",
"packages/extension",
"packages/fullstack",
"packages/fullstack-hooks",
"packages/fullstack-protocol",
"packages/generational-box",
"packages/history",
"packages/hooks",
Expand Down Expand Up @@ -102,7 +104,7 @@ members = [
"packages/playwright-tests/suspense-carousel",
"packages/playwright-tests/nested-suspense",
"packages/playwright-tests/cli-optimization",
"packages/playwright-tests/wasm-split-harness",
"packages/playwright-tests/wasm-split-harness"
]

[workspace.package]
Expand Down Expand Up @@ -141,6 +143,8 @@ dioxus-cli-opt = { path = "packages/cli-opt", version = "0.6.2" }
dioxus-devtools = { path = "packages/devtools", version = "0.6.2" }
dioxus-devtools-types = { path = "packages/devtools-types", version = "0.6.2" }
dioxus-fullstack = { path = "packages/fullstack", version = "0.6.2" }
dioxus-fullstack-hooks = { path = "packages/fullstack-hooks", version = "0.6.3" }
dioxus-fullstack-protocol = { path = "packages/fullstack-protocol", version = "0.6.3" }
dioxus_server_macro = { path = "packages/server-macro", version = "0.6.2", default-features = false }
dioxus-dx-wire-format = { path = "packages/dx-wire-format", version = "0.6.2" }
dioxus-logger = { path = "packages/logger", version = "0.6.2" }
Expand Down
6 changes: 4 additions & 2 deletions examples/fullstack-router/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ publish = false
[dependencies]
dioxus = { workspace = true, features = ["fullstack", "router"] }
axum = { workspace = true, optional = true }
tokio = {workspace = true, features = ["full"], optional = true }
tokio = { workspace = true, features = ["full"], optional = true }
serde = { version = "1.0.159", features = ["derive"] }
reqwest = { workspace = true, features = ["json"] }
http = { workspace = true, optional = true }

[features]
default = []
server = ["axum", "dioxus/server"]
server = ["axum", "dioxus/server", "dep:tokio", "dep:http"]
web = ["dioxus/web"]

124 changes: 79 additions & 45 deletions examples/fullstack-router/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,9 @@ use dioxus::prelude::*;

fn main() {
dioxus::LaunchBuilder::new()
.with_cfg(server_only!(ServeConfig::builder().incremental(
IncrementalRendererConfig::default()
.invalidate_after(std::time::Duration::from_secs(120)),
)))
.with_cfg(server_only!(
ServeConfig::builder().enable_out_of_order_streaming()
))
.launch(app);
}

Expand All @@ -24,62 +23,97 @@ enum Route {
#[route("/")]
Home {},

#[route("/blog/:id/")]
Blog { id: i32 },
#[route("/:breed")]
Breed { breed: String },
}

#[component]
fn Blog(id: i32) -> Element {
fn Home() -> Element {
rsx! {
Link { to: Route::Home {}, "Go to counter" }
table {
tbody {
for _ in 0..id {
tr {
for _ in 0..id {
td { "hello world!" }
}
}
}
}
Link { to: Route::Breed { breed: "hound".to_string() }, "Hound" }
}
}

#[component]
fn Breed(breed: String) -> Element {
rsx! {
BreedGallery { breed: "{breed}", slow: false }
SuspenseBoundary {
fallback: |_| rsx! { "Loading..." },
DoesNotSuspend {}
BreedGallery { breed, slow: true }
}
}
}

#[component]
fn Home() -> Element {
let mut count = use_signal(|| 0);
let mut text = use_signal(|| "...".to_string());
fn DoesNotSuspend() -> Element {
rsx! { "404" }
}

#[derive(serde::Deserialize, serde::Serialize)]
struct BreedResponse {
message: Vec<String>,
}

#[component]
fn BreedGallery(breed: ReadOnlySignal<String>, slow: bool) -> Element {
// use_server_future is very similar to use_resource, but the value returned from the future
// must implement Serialize and Deserialize and it is automatically suspended
let response = use_server_future(move || async move {
if slow {
#[cfg(feature = "server")]
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
}
#[cfg(feature = "server")]
{
use http::StatusCode;
let context = server_context();
let mut write = context.response_parts_mut();
write.status = StatusCode::NOT_FOUND;
write.extensions.insert("error???");
write.version = http::Version::HTTP_2;
write
.headers
.insert("x-custom-header", http::HeaderValue::from_static("hello"));
}
// The future will run on the server during SSR and then get sent to the client
reqwest::Client::new()
.get(format!("https://dog.ceo/api/breed/{breed}/images"))
.send()
.await
// reqwest::Result does not implement Serialize, so we need to map it to a string which
// can be serialized
.map_err(|err| err.to_string())?
.json::<BreedResponse>()
.await
.map_err(|err| err.to_string())
// use_server_future calls `suspend` internally, so you don't need to call it manually, but you
// do need to bubble up the suspense variant with `?`
})?;

// If the future was still pending, it would have returned suspended with the `?` above
// we can unwrap the None case here to get the inner result
let response_read = response.read();
let response = response_read.as_ref().unwrap();

// Then you can just handle the happy path with the resolved future
rsx! {
Link { to: Route::Blog { id: count() }, "Go to blog" }
div {
h1 { "High-Five counter: {count}" }
button { onclick: move |_| count += 1, "Up high!" }
button { onclick: move |_| count -= 1, "Down low!" }
button {
onclick: move |_| async move {
if let Ok(data) = get_server_data().await {
println!("Client received: {}", data);
text.set(data.clone());
post_server_data(data).await.unwrap();
display: "flex",
flex_direction: "row",
match response {
Ok(urls) => rsx! {
for image in urls.message.iter().take(3) {
img {
src: "{image}",
width: "100px",
height: "100px",
}
}
},
"Run server function!"
Err(err) => rsx! { "Failed to fetch response: {err}" },
}
"Server said: {text}"
}
}
}

#[server(PostServerData)]
async fn post_server_data(data: String) -> Result<(), ServerFnError> {
println!("Server received: {}", data);

Ok(())
}

#[server(GetServerData)]
async fn get_server_data() -> Result<String, ServerFnError> {
Ok("Hello from the server!".to_string())
}
8 changes: 8 additions & 0 deletions packages/core/src/global_context.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::prelude::SuspenseContext;
use crate::runtime::RuntimeError;
use crate::{innerlude::SuspendedFuture, runtime::Runtime, CapturedError, Element, ScopeId, Task};
use std::future::Future;
Expand Down Expand Up @@ -40,6 +41,13 @@ pub fn throw_error(error: impl Into<CapturedError> + 'static) {
.throw_error(error)
}

/// Get the suspense context the current scope is in
pub fn suspense_context() -> Option<SuspenseContext> {
current_scope_id()
.unwrap_or_else(|e| panic!("{}", e))
.suspense_context()
}

/// Consume context from the current scope
pub fn try_consume_context<T: 'static + Clone>() -> Option<T> {
Runtime::with_current_scope(|cx| cx.consume_context::<T>())
Expand Down
16 changes: 8 additions & 8 deletions packages/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,14 +96,14 @@ pub mod prelude {
fc_to_builder, generation, has_context, needs_update, needs_update_any, parent_scope,
provide_context, provide_error_boundary, provide_root_context, queue_effect, remove_future,
schedule_update, schedule_update_any, spawn, spawn_forever, spawn_isomorphic, suspend,
throw_error, try_consume_context, use_after_render, use_before_render, use_drop, use_hook,
use_hook_with_cleanup, with_owner, AnyValue, Attribute, Callback, Component,
ComponentFunction, Context, Element, ErrorBoundary, ErrorContext, Event, EventHandler,
Fragment, HasAttributes, IntoAttributeValue, IntoDynNode, OptionStringFromMarker,
Properties, ReactiveContext, RenderError, Runtime, RuntimeGuard, ScopeId, ScopeState,
SuperFrom, SuperInto, SuspendedFuture, SuspenseBoundary, SuspenseBoundaryProps,
SuspenseContext, SuspenseExtension, Task, Template, TemplateAttribute, TemplateNode, VNode,
VNodeInner, VirtualDom,
suspense_context, throw_error, try_consume_context, use_after_render, use_before_render,
use_drop, use_hook, use_hook_with_cleanup, with_owner, AnyValue, Attribute, Callback,
Component, ComponentFunction, Context, Element, ErrorBoundary, ErrorContext, Event,
EventHandler, Fragment, HasAttributes, IntoAttributeValue, IntoDynNode,
OptionStringFromMarker, Properties, ReactiveContext, RenderError, Runtime, RuntimeGuard,
ScopeId, ScopeState, SuperFrom, SuperInto, SuspendedFuture, SuspenseBoundary,
SuspenseBoundaryProps, SuspenseContext, SuspenseExtension, Task, Template,
TemplateAttribute, TemplateNode, VNode, VNodeInner, VirtualDom,
};
}

Expand Down
9 changes: 9 additions & 0 deletions packages/core/src/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,15 @@ impl Runtime {
result
}

/// Run a closure with the rendering flag set to false
pub(crate) fn while_not_rendering<T>(&self, f: impl FnOnce() -> T) -> T {
let previous = self.rendering.get();
self.rendering.set(false);
let result = f();
self.rendering.set(previous);
result
}

/// Create a scope context. This slab is synchronized with the scope slab.
pub(crate) fn create_scope(&self, context: Scope) {
let id = context.id;
Expand Down
12 changes: 11 additions & 1 deletion packages/core/src/scope_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,12 @@ impl Scope {
let mut hooks = self.hooks.try_borrow_mut().expect("The hook list is already borrowed: This error is likely caused by trying to use a hook inside a hook which violates the rules of hooks.");

if cur_hook >= hooks.len() {
hooks.push(Box::new(initializer()));
Runtime::with(|rt| {
rt.while_not_rendering(|| {
hooks.push(Box::new(initializer()));
});
})
.unwrap()
}

self.use_hook_inner::<State>(hooks, cur_hook)
Expand Down Expand Up @@ -620,4 +625,9 @@ impl ScopeId {
pub fn throw_error(self, error: impl Into<CapturedError> + 'static) {
throw_into(error, self)
}

/// Get the suspense context the current scope is in
pub fn suspense_context(&self) -> Option<SuspenseContext> {
Runtime::with_scope(*self, |cx| cx.suspense_boundary.suspense_context().cloned()).unwrap()
}
}
Loading
Loading