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 to stop useSuspenseQuery from running during static generation? #76

Open Tvrqvoise opened 9 months ago

Tvrqvoise commented 9 months ago

My API server is not accessible from my build server, so I get fetch errors when trying to render a component that has useSuspenseQuery at build time. It works fine during SSR and client render, just not during the static prerender.

I should note this is from within app/ not pages/, which is why I suspect a lot of the stuff I've tried isn't working.

If I have a component like

/app/page.tsx

and it imports a component like

'use client'

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

// the docs seems to imply that these will stop server rendering 
// like a dynamic slug will, but it does not work
export const dynamic = "force-dynamic";
export const revalidate = 0;

const query = gql`
  query SomeQuery($substring: String!) {
    /* whatever */
  }
`

export default function SearchInput () {
  const response: any = useSuspenseQuery(query);
  const results = response?.data?.query

  return (
    <div>{JSON.stringify(data)}</div>
  )
}

Then I get an error like this

$ npm run build

> client@0.1.0 build
> next build

- info Creating an optimized production build
- info Compiled successfully
- info Linting and checking validity of types
- info Collecting page data
[    ] - info Generating static pages (0/4)ApolloError: fetch failed

    ... stacktrace omitted ...

  graphQLErrors: [],
  protocolErrors: [],
  clientErrors: [],
  networkError: TypeError: fetch failed
      at Object.fetch (node:internal/deps/undici/undici:11576:11)
      at process.processTicksAndRejections (node:internal/process/task_queues:95:5) {
    cause: Error: getaddrinfo ENOTFOUND some-internal-url
        at GetAddrInfoReqWrap.onlookup [as oncomplete] (node:dns:108:26)
        at GetAddrInfoReqWrap.callbackTrampoline (node:internal/async_hooks:130:17) {
      errno: -3008,
      code: 'ENOTFOUND',
      syscall: 'getaddrinfo',
      hostname: 'some-internal-url'
    }
  },
  extraInfo: undefined
}

Error occurred prerendering page "/". Read more: https://nextjs.org/docs/messages/prerender-error
ApolloError: fetch failed

... stacktrace omitted ....

- info Generating static pages (4/4)

> Export encountered errors on following paths:
    /page: /
phryneas commented 9 months ago

I think you won't do yourself a favor if you skipped this: If you could skip it, upon rehydration in the browser, you would end up with a different DOM than was generated in a server, and React would end up with a "rehydration mismatch" error.

Workaround? Try a SchemaLink

So my first thought would be: can you make it possible in any way that that data can be reached? If that server runs as part of the same Next.js service, you can try to use a SchemaLink on the server (example in #36), which would avoid all HTTP calls.


If that is not possible, you might want to go one of those two routes:

Prevent any kind of SSR for this component

In this case, you would do something like

const [isSsrRender, setClientRender] = useState(true)
useEffect(() => setClientRender(true), [])
if (isSsrRender) return null

Throw an error on the server

You would wrap your component in an ErrorBoundary, and throw an error (replace your HttpLink with a link that always errors in SSG). On the server, this would not render your error fallback, but immediately try to render this on the client instead - without a rehydration mismatch.

Tvrqvoise commented 9 months ago

Hello! I am not trying to skip runtime SSR (which works appropriately, and is good for the reasons you mentioned) but merely skip build-time pre-rendering.

NextJS seems to run components in 3 contexts:

Put another way:

So all runtime calls work great, server and client alike. The problem is only that NextJS tries to run all the pages when I run next build and this is what fails.

I think I could potentially modify those examples to use an environment variable to detect which server environment I'm running in. I'll give it a shot and let you know.

plausible-phry commented 9 months ago

Sorry for the late reply, I'm writing this from my vacation account ;)

In all my tests, exporting export const dynamic = "force-dynamic"; from a page would force it to not be rendered in SSG, but only in SSR.

Alternatively, you could get the same result with

    link: new HttpLink({
        // ...
       fetchOptions: { cache: "no-store" },
    }),

As for really detecting if you are in SSR or SSG, we at some point had some tools for that, but removed them before release because we didn't rely on them and they accessed Nextjs internals. You can see them here: https://github.com/apollographql/apollo-client-nextjs/blob/8bdc0cf5746a50987732980920f4067aa8d71c5c/package/src/detectEnvironment.ts

hafaiedhmehdy commented 3 months ago

Ah, the chicken or the egg dilemma arises when using Next.js server integrations, such as apollo-server-integration-next. These are referred to as "microservices", and the issue at hand is that they do not run before the entire stack is built. This makes fetching anything on the "backend" somewhat impossible, which I consider a significant issue. Technically, you can run a perfect integration of Apollo Server and Apollo Client with Next.js in a single project during development; it's fantastic. However, the moment you build your project, you encounter a significant architectural challenge.

(Looking at the SchemaLink (example #36), this might be a step towards the right direction). πŸ‘€

What you might do is run a copy of your graph on another server through an environment variable, which you can switch out later. I'm genuinely curious about how this package handles context during build time. I'm somewhat confused, actually. Why is it fetching, and what is it attempting to pre-render since the data will vary every time anyway?

I don't believe @plausible-phry's answer is relevant to this issue, as we're discussing the build step:

Skip build-time pre-rendering. The failure occurs when I run next build.

// context is returned on query πŸͺ
const query_userIsAuthorized = gql`
    query {
        userIsAuthorized
    }
`
const { data: data_userIsAuthorized } = useSuspenseQuery(query_userIsAuthorized, { fetchPolicy: 'no-cache' })
// return true or false depending on the context 🫑
const userIsAuthorized = (data_userIsAuthorized as { userIsAuthorized: boolean }).userIsAuthorized 

How would something like this work if it were theoretically built?

phryneas commented 3 months ago

I don't believe @plausible-phry's answer is relevant to this issue, as we're discussing the build step:

I am pretty sure it is relevant?

The solution I proposed excludes the page in question from the build step - next.js recognizes that it has dynamic content and will not include it in the build step. It will be evaluated on the server when the first consumer tries to access the page.

You can do so either with

export const dynamic = "force-dynamic";

which will include the page from the start, or

  fetchOptions: { cache: "no-store" },

at which point Next.js will start building the page, but will notice at the time of the network request that it is dynamic, and skip the page then.

hafaiedhmehdy commented 3 months ago

Hello @phryneas, appreciate the prompt response!

// This code block serves as the baseline. src/app/api/graphql/route.ts

import { ApolloServer } from '@apollo/server'
import { gql } from '@apollo/client'
import { startServerAndCreateNextHandler } from '@as-integrations/next'

const resolvers = {
  Query: {
    retrieve: () => true
  }
}

const typeDefs = gql`
    type Query {
      retrieve: Boolean!
    }
`

const server = new ApolloServer({ resolvers, typeDefs })
const handler = startServerAndCreateNextHandler(server)

export { handler as GET, handler as POST }

// Dev βœ”οΈ, Build βœ”οΈ, Run βœ”οΈ
// This code block serves as control. src/app/page.tsx

'use client'

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

const query = gql`
  query Query {
    retrieve
  }
`

export default function Page() {

  const { loading, error, data } = useQuery(query)

  if (loading) return <p>Loading πŸ˜’</p>
  if (error) throw new Error('Failed to load data')
  if (typeof data.retrieve !== 'boolean') throw new Error('Invalid data type')

  return <p>Retrieve: {data.retrieve ? 'true βœ”οΈ' : 'false ❌'}</p>

}

// Dev βœ”οΈ, Build βœ”οΈ, Run βœ”οΈ
// This code block serves as the test cases. src/app/page.tsx

'use client'

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

const query = gql`
  query Query {
    retrieve
  }
`

export const dynamic = 'force-dynamic'
// Build: [ApolloError]: fetch failed with either of these three attempts.
// whether used in combination with force-dynamic or without it:

export default function Page() {

  const { data } = useSuspenseQuery<{ retrieve: boolean }>(query, { context: { fetchOptions: { cache: 'no-store' }}})
  // Build: [ApolloError]: fetch failed
  const { data } = useSuspenseQuery<{ retrieve: boolean }>(query, { fetchPolicy: 'no-cache' })
  // Build: [ApolloError]: fetch failed
  const { data } = useSuspenseQuery<{ retrieve: boolean }>(query, { context: { fetchOptions: { cache: 'no-store' } }, fetchPolicy: 'no-cache' })
  // Build: [ApolloError]: fetch failed
  return <p>Retrieve: {data.retrieve ? 'true βœ”οΈ' : 'false ❌'}</p>

}

// Dev βœ”οΈ, Build ❌, Run ❌

The techniques you've mentioned for handling dynamic content in Next.js, specifically using export const dynamic = "force-dynamic" and/or setting fetchOptions/fetchPolicy, are indeed about bypassing SSG or SSR mechanisms but do not skip the build steps. ☹️

I'm also considering whether this issue might be related to the behavior of apollo-client-nextjs itself or if it's an inherent limitation within Next.js.

phryneas commented 3 months ago

I'm also considering whether this issue might be related to the behavior of apollo-client-nextjs itself or if it's an inherent limitation within Next.js.

Huh, my memory might have been wrong on this part.

Yes, it's an inherent limitation of Next.js - your best bet is the SchemaLink, I guess.

hafaiedhmehdy commented 3 months ago
const { data } = useSuspenseQuery<{ retrieve: boolean }>(query, { fetchPolicy: 'no-cache' })
  return new NextSSRApolloClient({
    cache: new NextSSRInMemoryCache(),
    link: typeof window === 'undefined'
      ? ApolloLink.from([new SSRMultipartLink({ stripDefer: true }), new SchemaLink({ schema })])
      : httpLink,
  })

I need to thoroughly test this out and see if there are any caveats. While not disregarding the highlighted drawbacks of fetching one's own API endpoint in Next.js, one could argue that the concept of a monorepo 'self-fetching' its own graph feels intuitively correct for various reasons. This is especially true when data manipulation is centralized in a dedicated resolver rather than being dispersed across various pages; I really appreciate the nice abstraction and standardization that GraphQL offers 😍.

hmorri32 commented 2 months ago

Any news here? In the same boat as the OP.

phryneas commented 2 months ago

@hmorri32 that's a very unspecific question, since a lot was discussed in this issue, so I don't really know what to answer.

If your question is "I don't want to run useSuspenseQuery in static generation", the answer is probably still "don't render your component in static generation" or "don't use static generation" - because what should it do if it were not running useSuspenseQuery? The promise of useSuspenseQuery is that when your component renders, it has data, and when there is no data yet, your component suspends until it's there. If that suddenly were not the case during SSR, it would break the core promise of the API.

If your problem is "my server cannot call itself during static generation", that is still an inherent limitation of Next.js and we cannot magically fix that on our side, but you could use a SchemaLink in SSR instead of HTTP as a transport layer.

hmorri32 commented 2 months ago

Excuse my brevity. I'm deploying in a kubernetes context where I will not have the credentials necessary to fetch data via Apollo until actually deployed.

The output of next build is below.

 [ApolloError]: Response not successful: Received status code 401
    response: Response {
      [Symbol(realm)]: null,
      [Symbol(state)]: [Object],
      [Symbol(headers)]: [HeadersList]
    },
    statusCode: 401,
    result: 'Invalid IAP credentials: empty token'
  },
  extraInfo: undefined
}

Error occurred prerendering page "/applications". Read more: https://nextjs.org/docs/messages/prerender-error

I will only have IAP credentials once deployed and I'm looking for a way to have next build not fetch data during build time. So that I can deploy successfully. Bit of a catch 22.

phryneas commented 2 months ago

I'd say in that case, you don't want to build these pages - they should be SSRed later.

=> You would need to mark those pages as dynamic.