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

`NextSSRInMemoryCache` is not exported by `@apollo/experimental-nextjs-app-support/ssr` #262

Open nodegin opened 1 month ago

nodegin commented 1 month ago

In node_modules/.pnpm/@apollo+experimental-nextjs-app-support@0.9.1_@apollo+client@3.9.10_next@14.1.4_react@18.2.0/node_modules/@apollo/experimental-nextjs-app-support/dist/ssr/index.ssr.js

import { useContext } from 'react';
import { buildManualDataTransport } from '@apollo/client-react-streaming/manual-transport';
export { resetManualSSRApolloSingletons as resetNextSSRApolloSingletons } from '@apollo/client-react-streaming/manual-transport';
import { WrapApolloProvider, ApolloClient } from '@apollo/client-react-streaming';
export { DebounceMultipartResponsesLink, InMemoryCache as NextSSRInMemoryCache, RemoveMultipartDirectivesLink, SSRMultipartLink } from '@apollo/client-react-streaming';
import { ServerInsertedHTMLContext } from 'next/navigation.js';
export { useBackgroundQuery, useFragment, useQuery, useReadQuery, useSuspenseQuery } from '@apollo/client/index.js';

// src/ApolloNextAppProvider.ts

// src/bundleInfo.ts
var bundle = {
  pkg: "@apollo/experimental-nextjs-app-support/ssr",
  client: "NextSSRApolloClient",
  cache: "NextSSRInMemoryCache"
};

// src/ApolloNextAppProvider.ts
var ApolloNextAppProvider = /* @__PURE__ */ WrapApolloProvider(
  buildManualDataTransport({
    useInsertHtml() {
      const insertHtml = useContext(ServerInsertedHTMLContext);
      if (!insertHtml) {
        throw new Error(
          "ApolloNextAppProvider cannot be used outside of the Next App Router!"
        );
      }
      return insertHtml;
    }
  })
);
ApolloNextAppProvider.info = bundle;
var NextSSRApolloClient = class extends ApolloClient {
  /**
   * Information about the current package and it's export names, for use in error messages.
   *
   * @internal
   */
  static info = bundle;
};

export { ApolloNextAppProvider, NextSSRApolloClient };
//# sourceMappingURL=out.js.map
//# sourceMappingURL=index.ssr.js.map

NextSSRInMemoryCache is not exported.

I am using version 0.9.1

nodegin commented 1 month ago

Actually I am upgrading from 0.8.0 to 0.9.1 it became broken

NextSSRApolloClient is also not usable.

Error: __TURBOPACK__imported__module__$5b$project$5d2f$node_modules$2f2e$pnpm$2f40$apollo$2b$experimental$2d$nextjs$2d$app$2d$support$40$0$2e$9$2e$1_$40$apollo$2b$client$40$3$2e$9$2e$10_next$40$14$2e$1$2e$4_react$40$18$2e$2$2e$0$2f$node_modules$2f40$apollo$2f$experimental$2d$nextjs$2d$app$2d$support$2f$dist$2f$ssr$2f$index$2e$rsc$2e$js__$5b$app$2d$rsc$5d$__$28$ecmascript$29$__$7b$exports$7d$__.NextSSRApolloClient is not a constructor

I tried to import import * as test from '@apollo/experimental-nextjs-app-support/ssr'

and the result:

Object [Module] {
 DebounceMultipartResponsesLink: [Getter],
 RemoveMultipartDirectivesLink: [Getter],
 SSRMultipartLink: [Getter]
 }
phryneas commented 1 month ago

Hmm, it seems like you're getting the RSC entry point (where those shouldn't be available and using them was always a bug).

Do you still get that error when you mark the file importing it as "use client"?

Are you maybe building for another runtime such as edge? We got reports about that and I'm looking into that soon (just came back from vacation)

phryneas commented 1 month ago

Assuming you are using the edge runtime, I believe this will be solved by https://github.com/apollographql/apollo-client-nextjs/pull/263

Could you please give

npm i @apollo/experimental-nextjs-app-support@0.0.0-commit-84f7c8d

a try?

Giusti commented 1 month ago

The same issue occurs when importing registerApolloClient import { registerApolloClient } from "@apollo/experimental-nextjs-app-support/rsc";

just copy pasted the example from the github readme export const { getClient } = registerApolloClient(() => { return new ApolloClient({ ... }) }

phryneas commented 1 month ago

@Giusti you should be getting an error there if you are not importing registerApolloClient from a Server Component import tree - and that's intentional, as registerApolloClient can only safely be used in server components.

What environment are you using this in?

nodegin commented 1 month ago

I found the issue is that in version 0.9.0

the usage is changed, some exports is moved to another new package.

To fix the issue, simply change the import from:

import {
  NextSSRApolloClient,
  NextSSRInMemoryCache,
  SSRMultipartLink,
} from "@apollo/experimental-nextjs-app-support/ssr";

to:

import {
  ApolloClient,
  InMemoryCache,
  SSRMultipartLink,
} from '@apollo/client-react-streaming'

I also created a PR #269 to update the documents

nodegin commented 1 month ago

Closing this as I found the solution

phryneas commented 1 month ago

@nodegin as I commented in the PR, that's not the intended way of using it.

You should continue to use "@apollo/experimental-nextjs-app-support/ssr" - as you can see in the code that you yourself quoted up there, all those exports are still present:

export { DebounceMultipartResponsesLink, InMemoryCache as NextSSRInMemoryCache, RemoveMultipartDirectivesLink, SSRMultipartLink } from '@apollo/client-react-streaming';

import { ApolloClient } from '@apollo/client-react-streaming';

var NextSSRApolloClient = class extends ApolloClient {
  static info = bundle;
};
export { NextSSRApolloClient };
phryneas commented 1 month ago

If you are still encountering issues with that usage, we'll need to investigate that.

Is there any chance you can provide us with a reproduction of your issue?

nodegin commented 1 month ago

@phryneas I see, I think you can reproduce it by just installing 0.9.1 and navigate to @apollo/experimental-nextjs-app-support/dist/ssr/index.ssr.js, as I posted the exports are missing.

However changing to import from '@apollo/client-react-streaming' did fix for me, at least my app is running normally.

I guess the root cause could be some build config error casuing the re-export not working?

phryneas commented 1 month ago

the exports are missing.

That might be just a TypeScript error there, what your IDE sees and what the bundler sees are two very different things.

Are you importing from @apollo/experimental-nextjs-app-support/dist/ssr/index.ssr.js directly, or from @apollo/experimental-nextjs-app-support/ssr?

Depending on if you are in a RSC, SSR or Browser context, that import points to completely different files.

I do have an inkling though - you might have imports that accidentally pull Client files into RSC, or RSC files into Client Components. Could you read through #268 and see if that is similar to your situation?

phryneas commented 1 month ago

Especially thinking about your solution - @apollo/client-react-streaming exports ApolloClient in the RSC, SSR and Browser entry points, while @apollo/experimental-nextjs-app-support/ssr exports NextSSRInMemoryCache only in the SSR and Browser entrypoints (as in the past it was always considered a bug to use it from RSC).

I believe you have an import chain somewhere that pulls the file with your NextSSRInMemoryCache import into a RSC environment (and likewise, in @Giusti's case, an import chain that pulls the file with registerApolloClient into a Client Component environment).

nodegin commented 1 month ago

@phryneas Hmm, I see that could be the reason.

I have a apps/web/lib/apollo/index.ts in my setup:

export * from './apolloClient'
export * from './apolloProvider'

But that is just for simplifying the import statement, is that a problem?

How about if I import from @apollo/client-react-streaming as NextSSR* is basically just re-exporting.

The behavior was working fine before v0.9.0

phryneas commented 1 month ago

The behavior was working fine before v0.9.0

Yes, with 0.9.0 we aligned with how a React Server Component package should be packaged according to the React team. Their philosophy is "fail early on bundling, not late with a runtime error", which makes sense but doesn't give any good error messages :/

I'm honestly really sorry for these errors, but this is pretty much the experience it is meant to be :(

Your barrel exports here look very much like they could be the problem. I'd highly recommend not to switch over to the @apollo/client-react-streaming package for this, but to fix your bug here - you are likely ending up with a much bigger bundle size because you are bundling things that are not meant to be bundled.

phryneas commented 1 month ago

I did ask around for tools that might help with this on Twitter, and https://github.com/pahen/madge was suggested - maybe that helps you identify these imports faster.

JonasDoe commented 3 weeks ago

Especially thinking about your solution - @apollo/client-react-streaming exports ApolloClient in the RSC, SSR and Browser entry points, while @apollo/experimental-nextjs-app-support/ssr exports NextSSRInMemoryCache only in the SSR and Browser entrypoints (as in the past it was always considered a bug to use it from RSC).

I believe you have an import chain somewhere that pulls the file with your NextSSRInMemoryCache import into a RSC environment (and likewise, in @Giusti's case, an import chain that pulls the file with registerApolloClient into a Client Component environment).

Hm, I've got to confess, my understanding of SSR and its ways in Next.js 13+ are still lacking. But I'm facing the same error, even with a really minimal setup: https://github.com/JonasDoe/debug-apollo-client. There isn't a "use client" directive, and yet npm run build will result in the warning:

> build
> next build

  ▲ Next.js 14.2.2

   Creating an optimized production build ...
 ⚠ Compiled with warnings

./app/layout.tsx
Attempted import error: 'NextSSRInMemoryCache' is not exported from '@apollo/experimental-nextjs-app-support/ssr' (imported as 'NextSSRInMemoryCache').

Import trace for requested module:
./app/layout.tsx

...

This is not just a type error, since - in the real, deployed application -, calling it will result in an error when the @apollo/experimental-nextjs-app-support is greater version 0.8.0.

phryneas commented 3 weeks ago

@JonasDoe NextSSRInMemoryCache is an import for SSR only, which means "use client" files - you cannot import it from RSC, which you are doing.

There are three things in Next.js

So you'd use NextSSRInMemoryCache in Client Components (it needs to be used in SSR and Browser), but not in RSC.

There isn't a "use client" directive, and yet npm run build will result in the warning:

Exactly, your example is a React Server Component trying to import something that should only be used in SSR and Browser. In RSC, you'd just use ApolloClient from @apollo/client (as we show in our examples).

Using the wrong import was always a bug in your app, we just enforce it the way the React team intends it to be enforced (with missing imports) since 0.9.0.

JonasDoe commented 3 weeks ago

@phryneas Phew, thanks for the detailed explanation! So if I understand correcly, RSC is the "purest" form of a component and has the highest restrictions. It is encapsulated and doesn't need to synchronize with the client. It can reside in app. When it's done, it passes its data as RSC Payload to the SSR (i.e. a component in app) and indirectly to the CSR. When it comes to GraphQL data loading in a RSC, I utilize rsc/registerApolloClient from the experimental package - together with @apollo/client/ApolloClient -, to share the GraphQL client and it's cache between all RSCs in a single request (happens automagically due to the registerApolloClient logic).

In the next step (SSR), in your example you've added "use client" at the top to ensure that it won't accidentally become a RSC, i.e. you can safely use NextSSRApolloClient or NextSSRInMemoryCache. Is that correct?

SSR and CSR will, in turn, utilize NextSSRApolloClient and the alike. What's the gain compared to the ordinary @apollo/client/ApolloClient? I think I've read the SSR clients come with automatic cache hydration at the client side, but I can't really find anything about it right now. In this explanatory blog entry the "full snippet" looks like this:

// lib/client.js
import { HttpLink } from "@apollo/client";
import {
  NextSSRInMemoryCache,
  NextSSRApolloClient,
} from "@apollo/experimental-nextjs-app-support/ssr";
import { registerApolloClient } from "@apollo/experimental-nextjs-app-support/rsc";

export const { getClient } = registerApolloClient(() => {
  return new NextSSRApolloClient({
    cache: new NextSSRInMemoryCache(),
    link: new HttpLink({
      uri: "https://main--time-pav6zq.apollographos.net/graphql",
    }),
  });
});

So for some reason, RSC and SSR come together again, and it differs from the repo's README in that regard. So I'm a bit confused here.

Also, if I understand correctly, the @apollo/client/ApolloClient is completely decoupled from the experimental SSR client's. No cache data is shared between them and - if I understand RSC vs client component correctly - is not intended either. Is that right?

Using the wrong import was always a bug in your app, we just enforce it the way the React team intends it to be enforced (with missing imports) since 0.9.0.

I don't doubt that, but I wasn't experiencing any faulty behavior prior to 0.9.0, that's why I am a bit suprised.

phryneas commented 3 weeks ago

@phryneas Phew, thanks for the detailed explanation! So if I understand correcly, RSC is the "purest" form of a component and has the highest restrictions. It is encapsulated and doesn't need to synchronize with the client. It can reside in app. When it's done, it passes its data as RSC Payload to the SSR (i.e. a component in app) and indirectly to the CSR. When it comes to GraphQL data loading in a RSC, I utilize rsc/registerApolloClient from the experimental package - together with @apollo/client/ApolloClient -, to share the GraphQL client and it's cache between all RSCs in a single request (happens automagically due to the registerApolloClient logic).

That pretty much sums it up.

In the next step (SSR), in your example you've added "use client" at the top to ensure that it won't accidentally become a RSC, i.e. you can safely use NextSSRApolloClient or NextSSRInMemoryCache. Is that correct?

Yup.

SSR and CSR will, in turn, utilize NextSSRApolloClient and the alike. What's the gain compared to the ordinary @apollo/client/ApolloClient? I think I've read the SSR clients come with automatic cache hydration at the client side, but I can't really find anything about it right now. In this explanatory blog entry the "full snippet" looks like this:

If you use useSuspenseQuery or useBackgroundQuery, we transport the result over from SSR into the browser and ensure you won't encounter hydration errors. If you use useQuery, we prevent requests from happening on the server (because SSR only renders once, so all you'd see there is a loading indicator, but your server would still make an unneccessary request).

You can get more context in this RFC or in this conference talk.

// lib/client.js
import { HttpLink } from "@apollo/client";
import {
  NextSSRInMemoryCache,
  NextSSRApolloClient,
} from "@apollo/experimental-nextjs-app-support/ssr";
import { registerApolloClient } from "@apollo/experimental-nextjs-app-support/rsc";

export const { getClient } = registerApolloClient(() => {
  return new NextSSRApolloClient({
    cache: new NextSSRInMemoryCache(),
    link: new HttpLink({
      uri: "https://main--time-pav6zq.apollographos.net/graphql",
    }),
  });
});

So for some reason, RSC and SSR come together again, and it differs from the repo's README in that regard. So I'm a bit confused here.

🤦 I know you won't believe it, but I already fixed this blog article twice. This just seems to come back. I fixed it again and contacted some people internally to find out why it keeps switching back.

Also, if I understand correctly, the @apollo/client/ApolloClient is completely decoupled from the experimental SSR client's. No cache data is shared between them and - if I understand RSC vs client component correctly - is not intended either. Is that right?

Yes, quoting from the README:

❗️ We do handle "RSC" and "SSR" use cases as completely separate.
You should generally try not to have overlapping queries between the two, as all queries made in SSR can dynamically update in the browser as the cache updates (e.g. from a mutation or another query), but queries made in RSC will not be updated in the browser - for that purpose, the full page would need to rerender. As a result, any overlapping data would result in inconsistencies in your UI.
So decide for yourself, which queries you want to make in RSC and which in SSR, and don't have them overlap.

We will soon have a way of "prefetching for SSR/Browser in RSC" though, see #258.

I don't doubt that, but I wasn't experiencing any faulty behavior prior to 0.9.0, that's why I am a bit suprised.

Yeah, it doesn't really "error", but your bundle size might have been increased significantly, at least we had reports of that.

Again, I'm sorry for all of that. We will make some adjustments in the next version where we move the /ssr and /rsc entrypoints both into / and rename NextSSR* to just the plain names InMemoryCache and ApolloClient. If you import them in RSC, it will give you the plain ones from @apollo/client and if you import them in SSR/Browser, you'll get the SSR implementation.

That will hopefully make this a lot better.

JonasDoe commented 3 weeks ago

You can get more context in this RFC or in this conference talk

Wow, this the RFC is impressive and rather helpful for some more insights, thank you! (Btw. in "as React does not a mechanism of injecting data into the stream at arbitrary points in time" a verb is missing.)

If you use useQuery, we prevent requests from happening on the server (because SSR only renders once, so all you'd see there is a loading indicator, but your server would still make an unneccessary request).

So where's the place I'm supposed to fetch initial data that might change later? I'ld rather do some general fetches at server-side for a performance gain. The RSC does fetches at server side, but isn't really supposed to have overlapping queries with the SSR/CSR, as you quote. In the SSR stage, in turn, doing data fetches (via hooks, now) isn't supported and will be skipped. I would expect that the RSC should fetch this data, and I stringify and pass it to the SSR as initial cache. Is this what https://github.com/apollographql/apollo-client-nextjs/pull/258 is there for?

Again, I'm sorry for all of that.

I'm really grateful that you take your time to explain the framework to users! Not only here in comments, but also at so many other places, which is quite some uphill fighting.

phryneas commented 3 weeks ago

So where's the place I'm supposed to fetch initial data that might change later? [...] Is this what https://github.com/apollographql/apollo-client-nextjs/pull/258 is there for?

Yup. That, or you just use useSuspenseQuery in a client component, which will fetch the data in SSR, not RSC - but only on the first request, not on further navigation. (That's what you'll be using RSC+PreloadQuery from #258 for)