apollographql / apollo-client-nextjs

Apollo Client support for the Next.js App Router
https://www.npmjs.com/package/@apollo/experimental-nextjs-app-support
MIT License
358 stars 25 forks source link

How is `useSuspenseQuery` supposed to work with Next App Router `Loading` files? #142

Closed rob-strover-axomic closed 5 months ago

rob-strover-axomic commented 5 months ago

Hello,

Thanks for all your awesome work.

I'm using the Next JS 13 app router with the @apollo/experimental-nextjs-app-support/ssr useSuspenseQuery hook in one of my client component page.tsx files. When I load the page, I can see in the network tab in chrome that the server is responding with the loading.tsx content and doesn't seem to wait for the query to complete and render the page content before responding. Is this expected? If I remove the loading.tsx file then the server does wait for the query to complete and responds with the rendered page.tsx content. Ideally I do want a loading state to appear on the client side as a placeholder when navigating on the client side.

To summarise; Ideally, I want the server to wait for the query to complete, render the content and then respond. I would then want client side route changes to show the loading content whilst the frontend waits for the request to complete. Is this possible?

Thanks for taking the time to give thoughts here, is there something I'm missing?

Code samples below:

// ApolloProvider.tsx
"use client";

import React from 'react';

import { ApolloLink, HttpLink } from '@apollo/client';
import {
    ApolloNextAppProvider,
    NextSSRInMemoryCache,
    NextSSRApolloClient,
    SSRMultipartLink,
} from '@apollo/experimental-nextjs-app-support/ssr';

const makeClient = () => {
    const httpLink = new HttpLink({
        uri: '----',
        headers: { 'Access-Control-Allow-Origin': '*' },
        fetchOptions: { cache: "no-store" },
    });

    return new NextSSRApolloClient({
        cache: new NextSSRInMemoryCache(),
        link: typeof window === "undefined"
            ? ApolloLink.from([
                new SSRMultipartLink({
                    stripDefer: true,
                }),
                httpLink,
            ])
            : httpLink
    });
}

interface Props {
    children?: React.ReactNode;
}

const ApolloProvider = ({ children }: Props) => (
    <ApolloNextAppProvider makeClient={makeClient}>
        {children}
    </ApolloNextAppProvider>
);

export default ApolloProvider;
// root layout.tsx
import React from 'react';

import ApolloProvider from 'path/to/ApolloProvider';

interface RootLayoutProps {
    children: React.ReactNode;
    params: { lang: Locale };
}

export default async function RootLayout({
    children,
}: RootLayoutProps) {
    return (
        <html>
            <body>
                <ApolloProvider>
                    {children}
                </ApolloProvider>
            </body>
        </html>
    );
}
// page.tsx
'use client';

import React from 'react';

import { gql } from '@apollo/client';
import { useSuspenseQuery } from '@apollo/experimental-nextjs-app-support/ssr';

const PAGE_QUERY = gql`
    query GetData($id: ID!) {
        ...
    }
`;

export const dynamic = "force-dynamic";

export default function Page({ params }: { params: { id: string } }) {
    const { id } = params;

    const { error, data } = useSuspenseQuery<GetPageQuery, GetPageQueryVariables>(PAGE_QUERY, {
        variables: { id },
    })

    if (error || !data) {
          // failure ui here
    }

   return (
        <div>
            {data.foo.bars.map(bar => (....)}
       </div>
   );
// loading.tsx

"use client"

import React from 'react';

const Loading = () => {
    return <p>Loading...</p>
}

export default Loading
phryneas commented 5 months ago

The server should respond with the loading contents first, and then also render your page contents and deliver them to your browser - that's how it is meant to be in Next.js.

If you want more fine-grained control over this, you can add additional Suspense boundaries to your application, to determine which parts of your control suspend in which specific way.

rob-strover-axomic commented 5 months ago

@phryneas Thanks for your speedy response!

So am I right in saying there is no way to produce the use case I described above? Does this mean it is not possible to retrieve data in an SEO friendly way outside of serverside components?

phryneas commented 5 months ago

I believe you will get the same result with Server Components once you have a few Suspense boundaries in there.

This is React's "streaming render".

I think I read somewhere that Next detects if it's accessed by a crawler and doesn't do out-of-order streaming in that case, but I might be wrong.

phryneas commented 5 months ago

This discussion might be interesting to you: https://github.com/vercel/next.js/discussions/50829

rob-strover-axomic commented 5 months ago

@phryneas Thanks, this is helpful, strange how lighthouse seems to get a fully server side rendered version of the site back. I will try and simulate what's going on here with Postman.

I'm still confused as to how streaming pieces of UI rendered on the server is better than using the client to render content after the first load but maybe that answer will become clear as I keep going.

phryneas commented 5 months ago

It's a technical issue: RSC don't create HTML - so without a SSR pass of all your client components (that can intertwine with your RSC tree) your application cannot stream HTML to the browser at all.

At the same time, you want to start streaming data as fast as possible, so that's why that render is streamed.

rob-strover-axomic commented 5 months ago

So when I remove the loading files from my Next JS application, refresh the page and see that the actual page markup is returned in the response, how is this worse than streaming some loading spinner markup first and then the rendered data markup? Isn't this more work?

phryneas commented 5 months ago

A white screen instead of a customized loader (e.g. a skeleton view) is usually a worse user experience, and that's what you should optimize for, long before you optimize for SEO.

You should probably also set Suspense boundaries throughout your application, which would give you more granular loading states and would allow parts of your application to be interactive even while some others are still loading.

I can recommend giving this talk a watch: https://portal.gitnation.org/contents/how-to-use-suspense-and-graphql-with-apollo-to-build-great-user-experiences

rob-strover-axomic commented 5 months ago

Thanks for all your help and information here. :)