Skip to content

FOUC with components inside Suspense #408

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

Open
jerhat opened this issue May 1, 2025 · 6 comments
Open

FOUC with components inside Suspense #408

jerhat opened this issue May 1, 2025 · 6 comments
Labels
bug Something isn't working

Comments

@jerhat
Copy link
Contributor

jerhat commented May 1, 2025

Flash of unstyled content when using components inside Suspense.

Does not happen 100% of the time, but frequently enough:

Image

Minimal repo, with Thaw 0.4.6 and Leptos 0.7.8:

use leptos::prelude::*;
use leptos_meta::{provide_meta_context, MetaTags};
use thaw::ssr::SSRMountStyleProvider;
use thaw::{Button, ConfigProvider};

pub fn shell(options: LeptosOptions) -> impl IntoView {
    view! {
        <SSRMountStyleProvider>
            <!DOCTYPE html>
            <html lang="en">
                <head>
                    <meta charset="utf-8" />
                    <meta name="viewport" content="width=device-width, initial-scale=1" />
                    <AutoReload options=options.clone() />
                    <HydrationScripts options />
                    <MetaTags />
                </head>
                <body>
                    <App />
                </body>
            </html>
        </SSRMountStyleProvider>
    }
}

#[server]
async fn load_data(count: i32) -> Result<i32, ServerFnError> {
    tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
    Ok(count * 2)
}

#[component]
pub fn App() -> impl IntoView {
    provide_meta_context();
    let once = OnceResource::new(load_data(21));

    view! {
        <ConfigProvider>
            <Suspense fallback=move || {
                view! { <div></div> }.into_any()
            }>
            {move || Suspend::new(async move {
                match once.await {
                    Ok(count) => {
                        view!{
                            <Button>{count.to_string()}</Button>
                        }.into_any()
                    }
                    Err(_) => {
                        view!{
                            <div>"error"</div>
                        }.into_any()
                    }
                }
            })}
            </Suspense>
        </ConfigProvider>
    }
}
@lizidev lizidev added the bug Something isn't working label May 3, 2025
@lukexor
Copy link

lukexor commented May 4, 2025

I believe this is due to things like this e.g. in ProgressBar:

mount_style("progress-bar", include_str!("./progress-bar.css"));

Including this component outside of any Suspense renders properly in SSR. I'm not sure if any work-arounds short of including a hidden copy of the element outside of Suspense to get the css to load during SSR.

@lizidev
Copy link
Collaborator

lizidev commented May 10, 2025

Suspend calls the component asynchronously, while SSRMountStyleProvider can only collect styles synchronously, which leads to these problems.

I think of two solutions:

  1. Integrate Dioxus team's Manganis library for cargo-leptos.
  2. Use build.rs to collect all styles and generate a css file. Similar to the processing method of leptonic library.

Both of these solutions will deprecate SSRMountStyleProvider.

@lukexor
Copy link

lukexor commented May 10, 2025

Makes sense that there would would need to be some sort of pass like tailwind does to collect styles, but I'm confused if it's actually required.

The html content is there because cargo leptos runs all Suspense components once in SSR to know if it needs to suspend, so I'm not clear why that couldn't be used to pick up styles, using the Style component for example in leptos meta for example, which sticks any styles it finds in the head

@lukexor
Copy link

lukexor commented May 10, 2025

Also if it helps, I have been using a a bit of a hack that solves the issue by including specific thaw components above the Suspense boundary with a style of display:none which forces the stylesheets to load during SSR

@lizidev
Copy link
Collaborator

lizidev commented May 10, 2025

using the Style component for example in leptos meta for example, which sticks any styles it finds in the head

Because leptos_meta is deeply integrated with leptos_axum. Related code:

// The build_response function executes the `SSRMountStyleProvider` component
// (which completes the style collection here) 
// and the asynchronous function that collects `Suspend`.
let (owner, stream) = build_response(
    app_fn,
    additional_context,
    stream_builder,
    supports_ooo,
);

let sc = owner.shared_context().unwrap();

let stream = stream.await.ready_chunks(32).map(|n| n.join(""));

// Executes the asynchronous function collected by `Suspend`
while let Some(pending) = sc.await_deferred() {
    pending.await;
}

// `leptos_meta` will inject styles after the asynchronous functions collected by `Suspend` are executed
let mut stream = Box::pin(
    meta_context.inject_meta_context(stream).await.then({
        let sc = Arc::clone(&sc);
        move |chunk| {
            let sc = Arc::clone(&sc);
            async move {
                while let Some(pending) = sc.await_deferred() {
                    pending.await;
                }
                chunk
            }
        }
    }),
);```

@lukexor
Copy link

lukexor commented May 10, 2025

Ah! Thanks for the context. A build.rs seems fine, but just wondering what the impact would be for including a lot of css on conditional paths, when the source of truth for SSR is actually running the code.

Is there a possibility of adding a generic SSR injection mechanism similar to how leptos-use implements for use_cookie? Or just adding axum/actix feature flags for SSR?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

3 participants