Open Tvrqvoise opened 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.
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:
In this case, you would do something like
const [isSsrRender, setClientRender] = useState(true)
useEffect(() => setClientRender(true), [])
if (isSsrRender) return null
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.
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:
next start
)next build
) This is the only case which is broken.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.
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
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?
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.
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.
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.
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 π.
Any news here? In the same boat as the OP.
@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.
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.
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.
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/
notpages/
, which is why I suspect a lot of the stuff I've tried isn't working.If I have a component like
and it imports a component like
Then I get an error like this