Skip to content

PreloadQuery + useSuspenseQuery shows Suspense fallback in SSR HTML despite documentation stating it shouldn't #521

@longfellowone

Description

@longfellowone

Environment:

  • @apollo/client: 4.0.9
  • @apollo/client-integration-nextjs: 0.14.1
  • next: 16.0.1
  • react: 19.2.0

Description:

When using the recommended PreloadQuery + useSuspenseQuery pattern for Next.js App Router, the Suspense fallback always appears in the initial server-rendered HTML, causing a visible flash during page load.

According to the official documentation:

"During initial render, the fallback should NOT appear because data preloading completes server-side before hydration."

And:

"The Suspense boundary here is optional and only for demonstration purposes."

However, in practice, the fallback is consistently rendered in the SSR HTML.

Expected Behavior:

The Suspense fallback should not appear in the initial HTML response. The preloaded data should be available immediately during hydration, preventing the Suspense boundary from activating.

Actual Behavior:

The Suspense fallback appears in the SSR HTML:

<!--$?--><template id="B:0"></template><div>Loading products...</div><!--/$-->

This causes a visible flash as the page loads, then hydrates with the actual data.


Code Example

Server Component (app/page.tsx):

import { PreloadQuery } from "@/lib/apollo-client";
import ProductsList from "@/components/products-list";
import { GET_PRODUCTS } from "@/lib/queries/products";
import { Suspense } from "react";

export default function Home() {
  return (
    <PreloadQuery query={GET_PRODUCTS}>
      <Suspense fallback={<div>Loading products...</div>}>
        <ProductsList />
      </Suspense>
    </PreloadQuery>
  );
}

Client Component (components/products-list.tsx):

'use client';

import { useSuspenseQuery } from '@apollo/client/react';
import { GET_PRODUCTS, type Product } from '@/lib/queries/products';

export default function ProductsList() {
  const { data } = useSuspenseQuery<{ products: Product[] }>(GET_PRODUCTS);

  return (
    <div>
      <h1>Electrical Supply Products</h1>
      <ul>
        {data.products.map((product) => (
          <li key={product.id}>
            {product.name} - ${product.price.toFixed(2)}
          </li>
        ))}
      </ul>
    </div>
  );
}

Apollo Client Setup (lib/apollo-client.ts):

import { HttpLink } from "@apollo/client";
import {
  registerApolloClient,
  ApolloClient,
  InMemoryCache,
} from "@apollo/client-integration-nextjs";

export const { getClient, query, PreloadQuery } = registerApolloClient(() => {
  return new ApolloClient({
    cache: new InMemoryCache(),
    link: new HttpLink({
      uri: "http://localhost:8080/query",
    }),
  });
});

Client Wrapper (lib/apollo-wrapper.tsx):

"use client";

import { HttpLink } from "@apollo/client";
import {
  ApolloNextAppProvider,
  ApolloClient,
  InMemoryCache,
} from "@apollo/client-integration-nextjs";

function makeClient() {
  const httpLink = new HttpLink({
    uri: "http://localhost:8080/query",
  });

  return new ApolloClient({
    cache: new InMemoryCache(),
    link: httpLink,
  });
}

export function ApolloWrapper({ children }: { children: React.ReactNode }) {
  return (
    <ApolloNextAppProvider makeClient={makeClient}>
      {children}
    </ApolloNextAppProvider>
  );
}

Attempted Workarounds

  1. Importing the streaming module - Adding import "@apollo/client-react-streaming"; to preload the module (as suggested in SimulatePreloadedQuery suspends on mount because it's lazy loaded #506) did not resolve the issue.

  2. Props-based approach works - Using direct await getClient().query() in RSC and passing data as props eliminates the flash completely, but this bypasses the PreloadQuery pattern entirely.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions