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
449 stars 35 forks source link

registerApolloClient is not a function (Runtime error) #268

Closed NickVilaribov closed 7 months ago

NickVilaribov commented 7 months ago

Hey there. This is a first time I'm using Next JS 14 and Apollo client. I've done ApolloProvider and apollo-client.ts like in the documentation. In MainIntro I use an SSR request to preload the data that I use in the child component. The Header component must be client-side because I'm going to use hooks. As soon as I add "use client" to Header.tsx, I get the described error. Please help, below I post the component code and errors.

error

Screenshot 2024-04-03 at 13 26 34

apollo-client.ts

import { ApolloClient, HttpLink, InMemoryCache } from "@apollo/client";
import { registerApolloClient } from "@apollo/experimental-nextjs-app-support/rsc";

export const { getClient } = registerApolloClient(() => {
  return new ApolloClient({
    cache: new InMemoryCache(),
    link: new HttpLink({
      // this needs to be an absolute url, as relative urls cannot be used in SSR
      uri: process.env.API_URL,
      // you can disable result caching here if you want to
      // (this does not work if you are rendering your page with `export const dynamic = "force-static"`)
      // fetchOptions: { cache: "no-store" },
    }),
  });
});

apollo-wrapper.tsx

"use client";
// ^ this file needs the "use client" pragma

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

// have a function to create a client for you
function makeClient() {
  const httpLink = new HttpLink({
    // this needs to be an absolute url, as relative urls cannot be used in SSR
    uri: "http://localhost:3000/api/graphql",
    // you can disable result caching here if you want to
    // (this does not work if you are rendering your page with `export const dynamic = "force-static"`)
    fetchOptions: { cache: "no-store" },
    // you can override the default `fetchOptions` on a per query basis
    // via the `context` property on the options passed as a second argument
    // to an Apollo Client data fetching hook, e.g.:
    // const { data } = useSuspenseQuery(MY_QUERY, { context: { fetchOptions: { cache: "force-cache" }}});
  });

  return new NextSSRApolloClient({
    // use the `NextSSRInMemoryCache`, not the normal `InMemoryCache`
    cache: new NextSSRInMemoryCache(),
    link:
      typeof window === "undefined"
        ? ApolloLink.from([
            // in a SSR environment, if you use multipart features like
            // @defer, you need to decide how to handle these.
            // This strips all interfaces with a `@defer` directive from your queries.
            new SSRMultipartLink({
              stripDefer: true,
            }),
            httpLink,
          ])
        : httpLink,
  });
}

// you need to create a component to wrap your app in
export function ApolloWrapper({ children }: React.PropsWithChildren) {
  return (
    <ApolloNextAppProvider makeClient={makeClient}>
      {children}
    </ApolloNextAppProvider>
  );
}

lauout.tsx

export default function RootLayout({
  children,
  params: { locale },
}: Readonly<Props>) {
  return (
    <html lang={locale}>
      <body className={inter.className}>
        <ApolloWrapper>
          <Header />
          <main>{children}</main>
          <Footer />
        </ApolloWrapper>
      </body>
    </html>
  );
}

page.tsx

import { MainIntro } from "@/components";

const HomePage = () => {

  return (
    <>
      <MainIntro />
    </>
  );
};

export default HomePage;

MainIntro.tsx

import { gql } from "@apollo/client";
import { getTranslations, getMessages } from "next-intl/server";
import { NextIntlClientProvider } from "next-intl";
import pick from "lodash/pick";
import { getClient } from "@/configs";
import { Container } from "@/components";
import { FilterClient } from "./components";
import styles from "./MainIntro.module.css";

const MainIntro = async () => {
  const t = await getTranslations("Homepage");
  const messages = await getMessages();
  const { data } = await getClient().query({
    query: FILTER_QUERY,
    context: {
      fetchOptions: {
        next: { revalidate: 3600 },
      },
    },
  });

  return (
    <section className={styles.section}>
      <Container className={styles.container}>
        <h1 className={styles.title}>{t("title")}</h1>
        <p className={styles.description}>
          Discover your perfect match in healthcare with our service, where
          finding a trusted specialist is both convenient and swift, ensuring
          peace of mind with every choice
        </p>
        <NextIntlClientProvider messages={pick(messages, "Global")}>
          <FilterClient data={data} />
        </NextIntlClientProvider>
      </Container>
    </section>
  );
};

const FILTER_QUERY = gql`
  query Query {
    countries {
      id
      code
    }
    specialties {
      id
      key
    }
  }
`;

export default MainIntro;

header.tsx

"use client"
import React from "react";
import Link from "next/link";
import Image from "next/image";
import styles from "./header.module.css";
import { Container } from "@/components";
import { LangSwitcher } from "./components";

type Props = {
  simple?: boolean;
};

const Header:React.FC<Props> = ({ simple }) => {
  return (
    <header className={styles.section}>
      <Container className={styles.wrapper}>
        <Link href="/" className={styles.logo} aria-label="Veso Health Logo">
          <Image
            src="/veso-logo.svg"
            width={103}
            height={34}
            alt="Veso Health Logo"
            priority={true}
          />
        </Link>
        {!simple && <LangSwitcher />}
      </Container>
    </header>
  );
};

export default Header;
phryneas commented 7 months ago

This would happen if you import registerApolloClient from a Client Component import tree.

Do you import apollo-client.ts from a file that is marked as "use client", or from a file that imports from a file (etc.) that is marked as "use client"?

NickVilaribov commented 7 months ago

This would happen if you import registerApolloClient from a Client Component import tree.

Do you import apollo-client.ts from a file that is marked as "use client", or from a file that imports from a file (etc.) that is marked as "use client"?

No,

Screenshot 2024-04-03 at 13 39 36
phryneas commented 7 months ago

Are you building for Edge functions?

NickVilaribov commented 7 months ago

Are you building for Edge functions?

Sorry, don't get it. What is Edge functions? It's really small app, I use only next-intl and apollo client.

phryneas commented 7 months ago

Okay, obviously not :)

I think we need to get back to my previous question:

I see that you re-export getClient from "@/configs" - do you ever import "@/configs" from a "use client" file, or from a file that is imported from a "use client" file, and so on?

NickVilaribov commented 7 months ago

@/configs

No, I only use import from "@/configs" in Layout.tsx and MainIntro.tsx. And both of them don't use "use client". It's really strange, inside MainIntro.tsx i use FilterClient.tsx components and it's client component.

NickVilaribov commented 7 months ago

@phryneas https://github.com/NickVilaribov/veso-test

phryneas commented 7 months ago

Something seems to pull your server component file into client components here :/

Generally, Next.js build two independent versions of your app:

Internally, these import completely different files for the same import statement.

So, if you call import { ... } from '@apollo/experimental-app-support/rsc', from the "RSC side", it points to the file which contains the registerApolloClient method. If you call the same import from the "Client side", it just points to an empty file, because registerApolloClient uses React.cache, which works only in React Server Components (and also doesn't make sense in Client Components).
That's that you experience here right now.

React itself does the same. If you import { ... } from 'react' from the Client side, you will be able to import useState - but if you import it from the RSC side, useState won't be there.

NickVilaribov commented 7 months ago

Got it. It happens when I add "use client" on the Header.tsx file. Without this all works fine. Do you have any ideal how to solve it?

phryneas commented 7 months ago

Your Header.tsx imports from "@/components", which imports from "./main-intro", which imports from "./MainIntro", which imports from "@/configs" which calls import { registerApolloClient } from "@apollo/experimental-nextjs-app-support/rsc".

You have to stop using these index.ts files that mix server components and client components.

NickVilaribov commented 7 months ago

Your Header.tsx imports from "@/components", which imports from "./main-intro", which imports from "./MainIntro", which imports from "@/configs" which calls import { registerApolloClient } from "@apollo/experimental-nextjs-app-support/rsc".

You have to stop using these index.ts files that mix server components and client components.

HM. I guess I catch the problem. Header.tsx works with "use client" when I remove from it. what's wrong with Container.tsx ?

NickVilaribov commented 7 months ago

@phryneas You are right, another import solve it.

import { Container } from "@/components/ui";
//import { Container } from "@/components";
phryneas commented 7 months ago

Exactly that :)

NickVilaribov commented 7 months ago

But why? =) This type of import isn't a mistake, it works, but not in the Next.js.

phryneas commented 7 months ago

Your @/components/index.ts file re-exports from files that are both RSC and Client Components. You can never mix them together.

I would really recommend that you never use index.ts files like that to have a "nicer import", because all you do is pull files into your build that shouldn't be there.

phryneas commented 7 months ago

A Client Component is never allowed to import from a file that contains a React Server Component, because that "makes it a Client Component". In some cases it's just a bundling error, in some cases it can even pose a security risk.

NickVilaribov commented 7 months ago

A Client Component is never allowed to import from a file that contains a React Server Component, because that "makes it a Client Component". In some cases it's just a bundling error, in some cases it can even pose a security risk.

Thanks for your help

github-actions[bot] commented 7 months ago

Do you have any feedback for the maintainers? Please tell us by taking a one-minute survey. Your responses will help us understand Apollo Client usage and allow us to serve you better.