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 do i re-create the client with <ApolloNextAppProvider />? #103

Open zfogg opened 7 months ago

zfogg commented 7 months ago

I'm using and I to update the makeClient function, but when i update the function, the component doesn't make a new client :( how can i do that? i have dependencies that are retrieved during runtime (an auth token) and i want to recreate the client when i get the token on login, but since it already rendered the token remains null. does that make sense?

zfogg commented 7 months ago

i actually solve this here: https://github.com/zfogg/apollo-client-nextjs/blob/main/package/src/ssr/ApolloNextAppProvider.tsx (here's a diff)

i added a prop called "clientIndex" which is a string. if you change the value of the string, the apollo client will be recreated because makeClient() gets called again when that happens

this is useful logic that other people probably need! should i make a pull request? should it be done differently?

phryneas commented 7 months ago

I'm very sorry to say this, but I'd prefer you didn't recreate the client at all. In a normal, long-running application, this is something you should never need to do (and doing so has all kinds of drawbacks).

What is the reason you want to recreate the client in the first place?

zfogg commented 7 months ago

in my app, makeClient runs before an auth token that the makeClient needs to authenticate with my API is available. during my login flow, i receive this token, and then i need to make sure makeClient runs with it available. but ApolloNextAppProvider is already rendered and you give no way to allow my to decide exactly when the client gets created, because you use React.useRef which ALWAYS creates it during first render, even if makeClient doesn't have what it needs yet. what if makeClient doesn't have its necessary dependencies at the time of first render? this is my situation.

my fork actually solves my problem, and makeClient gets re-run after my login auth flow. this lets me decide when makeClient runs. your component currently does not. i only need to re-create the client once (once the auth token comes in) so this works for me

zfogg commented 7 months ago

basically, i want control over when the client is created because the client might depend on something that i get from a component that is rendered inside this wrapper. this is my situation

phryneas commented 7 months ago

In that case I would recommend that you use e.g. a ref (or another non-global value that can be modified) to hold that token and move the creation of your Link into a scope where you can access that ref. That way, you can later modify the ref whenever your token changes, without having to recreate the ApolloClient or Link instance itself.

phryneas commented 7 months ago

Could be something like



function makeClientWithRef(ref) {
  return function makeClient() {
    // if you access `ref.current` from your `Link` here it will always be up to date with your React component.
  }
}

function MyWrapper(props){
  const ref = useRef()
  const authToken = useAuthToken()
  ref.current = authToken

  return <ApolloNextAppProvider makeClient={() => makeClientWithRef(ref)}>{props.children}</ApolloNextAppProvider>

}
zfogg commented 7 months ago

i'll look into this! thanks

zfogg commented 7 months ago

will close the issue if it works

zfogg commented 7 months ago

i'm a little skeptical this will work because i use the token inside of setContext though. that still won't run again if my ref changes, will it?

phryneas commented 7 months ago

If you access the ref inside of setContext, it will give you the value it has at that point in time - whenever a request is made.

But of course, changing the ref won't re-run all your requests - which usually also is not really desirable - if a user token times out and refreshes, you don't want to throw away the full cache and rerun every query after all. It will only affect future queries.

zfogg commented 7 months ago

but the value of my ref is concatenated into a string when makeClient is run with my setContext call. so unless makeClient is run again, the value of the string that my ref was concatenated into won't change

zfogg commented 7 months ago

so i need makeClient to run again

zfogg commented 7 months ago
image

see? if i use a ref.current value here, even if i update the ref the value, the string with my Bearer ${token} won't change because it will be saved in memory after the first time makeClient runs. I need makeClient to run again so this string will be concatenated again with the auth token after login. using a ref won't help me here, unless i'm mistaken

mvandergrift commented 7 months ago

If authLink is set up correctly, setContext should be run on every request.

Can you do something like this (sorry, it's a rough estimate, I'm not in front of my workstation right now) and append it to your clients link collection.

  const authLink = new ApolloLink((operation, forward) => {
      operation.setContext(({ headers }) => ({
          headers: {
              authorization: `Bearer ${ref.current}`, // ref from your wrapper
              ...headers
          }
      }));
      return forward(operation);
  });

 const makeClient = () => (
        new NextSSRApolloClient({      
            link: ApolloLink.from([retryLink, authLink, logLink]), // whatever steps you have in your link chain
            cache: new NextSSRInMemoryCache()
        })
    );
phryneas commented 7 months ago

Exactly that. The setContext callback function will run for every request, and can set different headers for every request. So once ref.current updates, every future request will have the new token.

Sashkan commented 6 months ago

I managed to do so and make authenticated requests on both client and server components with the following:

// graphql.ts (exports the server method)
import { HttpLink } from "@apollo/client";
import { setContext } from "@apollo/client/link/context";
import { registerApolloClient } from "@apollo/experimental-nextjs-app-support/rsc";
import {
  NextSSRApolloClient,
  NextSSRInMemoryCache,
} from "@apollo/experimental-nextjs-app-support/ssr";

import { ACCESS_TOKEN_COOKIE_NAME } from "constants/cookies";
import { getCookie } from "utils/cookies";

// Get API URL
const apiBaseUrl = process.env.NEXT_PUBLIC_API_BASE_URL;

// Create an HTTP link with the GraphQL API endpoint
const httpLink = new HttpLink({
  uri: `${apiBaseUrl}/graphql`,
  // Disable result caching
  // fetchOptions: { cache: "no-store" },
});

// Create an authentication link
const authLink = setContext(async () => {
  // Get access token stored in cookie
  const token = await getCookie(ACCESS_TOKEN_COOKIE_NAME);

  // If the token is not defined, return an empty object
  if (!token?.value) return {};

  // Return authorization headers with the token as a Bearer token
  return {
    headers: {
      authorization: `Bearer ${token.value}`,
    },
  };
});

/**
 * Apollo Client
 *
 * @see https://www.apollographql.com/blog/apollo-client/next-js/how-to-use-apollo-client-with-next-js-13/
 */
// eslint-disable-next-line import/prefer-default-export
export const { getClient } = registerApolloClient(
  () =>
    new NextSSRApolloClient({
      cache: new NextSSRInMemoryCache(),
      link: authLink.concat(httpLink),
    }),
);

And here is the client wrapper

"use client";

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

import { ACCESS_TOKEN_COOKIE_NAME } from "constants/cookies";
import { getCookie } from "utils/cookies";

// Get API URL
const apiBaseUrl = process.env.NEXT_PUBLIC_API_BASE_URL;

// Create an HTTP link with the GraphQL API endpoint
const httpLink = new HttpLink({
  uri: `${apiBaseUrl}/graphql`,
});

// Create an authentication link
const authLink = setContext(async () => {
  // Get access token stored in cookie
  const token = await getCookie(ACCESS_TOKEN_COOKIE_NAME);

  // If the token is not defined, return an empty object
  if (!token?.value) return {};

  // Return authorization headers with the token as a Bearer token
  return {
    headers: {
      authorization: `Bearer ${token.value}`,
    },
  };
});

/**
 * Create an Apollo Client instance with the specified configuration.
 */
function makeClient() {
  return new NextSSRApolloClient({
    cache: new NextSSRInMemoryCache(),
    link:
      typeof window === "undefined"
        ? ApolloLink.from([
            new SSRMultipartLink({
              stripDefer: true,
            }),
            authLink.concat(httpLink),
          ])
        : authLink.concat(httpLink),
  });
}

/**
 * Apollo Provider
 *
 * @see https://www.apollographql.com/blog/apollo-client/next-js/how-to-use-apollo-client-with-next-js-13/
 */
export default function ApolloProvider({ children }: React.PropsWithChildren) {
  return (
    <ApolloNextAppProvider makeClient={makeClient}>
      {children}
    </ApolloNextAppProvider>
  );
}

These two work just fine, all my subsequent requests are authenticated through the bearer token. I'm still working on a cleaner version though, I'd like to extract the token getter logic so that I don't have to retrieve the token every time.

phryneas commented 6 months ago

@Sashkan could you share that getCookie function? Generally, the problem is that in a client component that is rendering on the server you don't have any access to cookies per default, so I wonder how you worked around that.

giovannetti-eric commented 6 months ago

Hello there!

I'm also quite stuck with the same kind of problem.

I'm on the latest version of each packages:

I have 2 things to solve:

Currently, I have this implementation, and I'm not able to update these headers values without reload the entire app.

"use client";

import { setContext } from "@apollo/client/link/context";
import { ApolloLink } from "@apollo/client/link/core";
import { onError } from "@apollo/client/link/error";
import {
  ApolloNextAppProvider,
  NextSSRApolloClient,
  NextSSRInMemoryCache,
  SSRMultipartLink,
} from "@apollo/experimental-nextjs-app-support/ssr";
import { createUploadLink } from "apollo-upload-client";
import { useParams } from "next/navigation";
import { MutableRefObject, useRef } from "react";

import { i18n, Locale } from "@/app/_libs/i18n/config";
import { useAuth } from "@/app/_providers/AuthContext";

function createClient(
  localeRef: MutableRefObject<string>,
  accessTokenRef: MutableRefObject<string | null>,
  logout: ({ returnTo }: { returnTo?: boolean }) => void,
) {
  const authLink = setContext(async (_, { headers }) => {
    console.log("authLink", localeRef.current);

    return {
      headers: {
        ...headers,
        "Accept-Language": localeRef.current ?? i18n.defaultLocale,
        ...(accessTokenRef.current
          ? { authorization: `Bearer ${accessTokenRef.current}` }
          : {}),
      },
    };
  });

  const errorLink = onError(({ graphQLErrors, networkError }) => {
    if (graphQLErrors) {
      graphQLErrors.forEach(({ message, locations, path, extensions }) => {
        console.error("[GraphQL error]", {
          Message: message,
          Location: locations,
          Path: path,
          Code: extensions?.code,
          Status: extensions?.status,
        });

        if (extensions?.status === "unauthorized") {
          logout({ returnTo: true });
        }
      });
    }
    if (networkError) console.error(`[Network error]: ${networkError}`);
  });

  const uploadLink = createUploadLink({
    uri: `${process.env.NEXT_PUBLIC_BASE_URL_API}/graphql`,
    fetchOptions: { cache: "no-store" },
  });

  const linkArray = [authLink, errorLink, uploadLink] as ApolloLink[];

  return new NextSSRApolloClient({
    cache: new NextSSRInMemoryCache({
      typePolicies: {
        TeamPlaybook: {
          keyFields: ["id", "teamId"],
        },
        TeamChapter: {
          keyFields: ["id", "teamId"],
        },
        TeamTheme: {
          keyFields: ["id", "teamId"],
        },
        TeamPractice: {
          keyFields: ["id", "teamId"],
        },
      },
    }),
    link:
      typeof window === "undefined"
        ? ApolloLink.from([
            new SSRMultipartLink({
              stripDefer: true,
            }),
            ...linkArray,
          ])
        : ApolloLink.from(linkArray),
    connectToDevTools: true,
    defaultOptions: {
      query: {
        errorPolicy: "all",
      },
      mutate: {
        errorPolicy: "all",
      },
    },
  });
}

export function ApolloWrapper({ children }: { children: React.ReactNode }) {
  const params = useParams();
  const lang = params?.lang as Locale;

  const localeRef = useRef<string>(lang || i18n.defaultLocale);
  localeRef.current = lang || i18n.defaultLocale;

  const accessTokenRef = useRef<string | null>(null);
  const { accessToken } = useAuth();
  accessTokenRef.current = accessToken;

  const { logout } = useAuth();

  console.log("ApolloWrapper", localeRef.current);

  return (
    <ApolloNextAppProvider
      makeClient={() => createClient(localeRef, accessTokenRef, logout)}
    >
      {children}
    </ApolloNextAppProvider>
  );
}

When I update the lang param in the url, console.log("ApolloWrapper", localeRef.current); display the new language, but console.log("authLink", localeRef.current); is still giving the previous one and when navigating, all the new queries have also the old language value in the headers. I have the exact same problem with my access token.

Bonus question: Since useAuth is taking the token from a cookie, and there is no access to the cookies in a client component during the server rendering, I get 401 unauthorized response for all my queries that need authentication. To bypass that I check for each query if the accessToken exist, and if the hook is used in a layout, I must store the data in a state with an effect to avoid an hydration error, it's really painful:

export function useTeam(teamId?: string) {
  const { accessToken } = useAuth();

  const { data } = useSuspenseQuery<{ team: Team }, { teamId: string }>(
    GET_TEAM,
    accessToken && teamId
      ? { variables: { teamId }, fetchPolicy: "cache-and-network" }
      : skipToken,
  );

  const team = data?.team;

  return {
    team,
  };
}
export default function NavigationTeamPopover() {
  const params = useParams();
  const teamId = params?.teamId as string;

  const [teamInfo, setTeamInfo] = useState<Team | null>(null);

  const { team } = useTeam(teamId);

  useEffect(() => {
    setTeamInfo(team || null);
  }, [team]);

  return (
    <CustomPopover
      label={
        <>
          <CustomAvatar
            type="team"
            size="xs"
            imageUrl={teamInfo?.imageUrl}
            label={teamInfo?.name}
          />
          {teamInfo?.name ? (
            <div
              className="w-full truncate font-medium"
              data-testid="navigation-team-popover-label"
            >
              {teamInfo.name}
            </div>
          ) : (
            <TextLoadingState className="w-full bg-gray-700" />
          )}
          <ExpandIcon className="h-3.5 w-3.5 flex-none" />
        </>
      }
      buttonClassName="min-h-[2.25rem]"
      placement="bottom-start"
      theme="dark"
      testId="navigation-team-popover"
    >
      <div className="flex max-w-[20rem] flex-col gap-2">
        <NavigationTeamSwitcher />
      </div>
    </CustomPopover>
  );
}

Am I doing something wrong? Did a better pattern exist to handle that?

phryneas commented 6 months ago

Hey there! We just shipped @apollo/client version 3.9.0-alpha.3, which contains an API that will make this a lot easier:

client.defaultContext

The idea here is that you can modify the defaultContext token of your ApolloClient instance at any time, and that will be available in your link chain.

So here you could do something like this:

// you can optionally enhance the `DefaultContext` like this to add some type safety to it.
declare module '@apollo/client' {
  export interface DefaultContext {
    token?: string
  }
}

function makeClient() {
  const authLink = setContext(async (_, { headers, token }) => {
    return {
      headers: {
        ...headers,
        ...(token ? { authorization: `Bearer ${token}` } : {}),
      },
    };
  });

  return new NextSSRApolloClient({
    cache: new NextSSRInMemoryCache(),
    link: authLink.concat(new HttpLink({ uri: 'https://example.com/graphl' }))
  });
}

export function ApolloWrapper({ children }: { children: React.ReactNode }) {
  return (
    <ApolloNextAppProvider makeClient={makeClient}>
      <UpdateAuth>
        {children}
      </UpdateAuth>
    </ApolloNextAppProvider>
  );
}

function UpdateAuth({ children }: { children: React.ReactNode }) {
  const token = useAuthToken();
  const apolloClient = useApolloClient()
  // just synchronously update the `apolloClient.defaultContext` before any child component can be rendered
  // so the value is available for any query started in a child
  apolloClient.defaultContext.token = token
  return <>{children}</>;
}

This would of course also work for more things than just the token, e.g. for that language selection that @giovannetti-eric is trying to implement here.

It does not solve the problems of accessing cookies in a SSRed client component though - that's something that Next.js has to solve on their side, unfortunately.

zfogg commented 6 months ago

thanks SO much for getting into this problem after i pointed out my issue! i really appreciate the responsiveness. the new solution looks awesome but your previous solution actually works fine for me. i'm glad to know there's a more elegant way built into the app now. you can close this issue if you deem it solved by this! my issue i solved and i'm no longer using my fork 😄

phryneas commented 6 months ago

Let's leave this open for visibility for now - it seems quite a lot of people are landing here :)

Sashkan commented 6 months ago

@phryneas Thanks a lot 🙏 Can I use it to update both clients ? The one returned by the useApolloClient hook and the one returned by the experimental registerApolloClient method ? Since I'm using both client and server components in my app, I want to make sure that the token is properly updated in both use cases 🤔

phryneas commented 6 months ago

@Sashkan you theoretically could, but in your Server Components (and also Server-rendered Client Components), you'll have a new Apollo Client instance for every request the user makes, so I wouldn't expect any token changes to happen there.

zfogg commented 6 months ago

unless you authenticate and then make another request within the same server-side code... that's possible. then they'd go from unauthed to authed and the apollo client would need to update with the token before making the second request and returning to the client

mikew commented 5 months ago

Hey there! We just shipped @apollo/client version 3.9.0-alpha.3, which contains an API that will make this a lot easier:

client.defaultContext

The idea here is that you can modify the defaultContext token of your ApolloClient instance at any time, and that will be available in your link chain.

So here you could do something like this:

// you can optionally enhance the `DefaultContext` like this to add some type safety to it.
declare module '@apollo/client' {
  export interface DefaultContext {
    token?: string
  }
}

function makeClient() {
  const authLink = setContext(async (_, { headers, token }) => {
    return {
      headers: {
        ...headers,
        ...(token ? { authorization: `Bearer ${token}` } : {}),
      },
    };
  });

  return new NextSSRApolloClient({
    cache: new NextSSRInMemoryCache(),
    link: authLink.concat(new HttpLink({ uri: 'https://example.com/graphl' }))
  });
}

export function ApolloWrapper({ children }: { children: React.ReactNode }) {
  return (
    <ApolloNextAppProvider makeClient={makeClient}>
      <UpdateAuth>
        {children}
      </UpdateAuth>
    </ApolloNextAppProvider>
  );
}

function UpdateAuth({ children }: { children: React.ReactNode }) {
  const token = useAuthToken();
  const apolloClient = useApolloClient()
  // just synchronously update the `apolloClient.defaultContext` before any child component can be rendered
  // so the value is available for any query started in a child
  apolloClient.defaultContext.token = token
  return children;
}

This would of course also work for more things than just the token, e.g. for that language selection that @giovannetti-eric is trying to implement here.

It does not solve the problems of accessing cookies in a SSRed client component though - that's something that Next.js has to solve on their side, unfortunately.

This is working wonderfully, but only for queries? I set up a simple lazy query that runs in an effect on the client, and can see our token getting passed to the query, but when we run a mutation, the token is missing.

Relevant code:

// authLink.ts
import { setContext } from '@apollo/client/link/context'

declare module '@apollo/client' {
  export interface DefaultContext {
    token?: string | null
  }
}

export const authLink = setContext(async (_graphqlRequest, context) => {
  return {
    headers: {
      ...context.headers,
      ...(context.token ? { authorization: context.token } : {}),
    },
  }
})
// ApolloContextUpdater.tsx
'use client'

import { useApolloClient } from '@apollo/client'

interface ApolloContextUpdaterProps {
  token?: string | null | undefined
}

const ApolloContextUpdater: React.FC<ApolloContextUpdaterProps> = (props) => {
  const client = useApolloClient()
  client.defaultContext.token = props.token

  return null
}

export default ApolloContextUpdater
// makeClient.ts
function makeClient() {
  const httpLink = new HttpLink({
    uri: process.env.NEXT_PUBLIC_GRAPHQL_URL,
    fetchOptions: { cache: 'no-store' },
  })

  return new NextSSRApolloClient({
    cache: new NextSSRInMemoryCache(),
    link:
      typeof window === 'undefined'
        ? ApolloLink.from([
            new SSRMultipartLink({
              stripDefer: true,
            }),
            authLink,
            httpLink,
          ])
        : ApolloLink.from([authLink, httpLink]),
  })
}
phryneas commented 5 months ago

@mikew thank you for the report - that will be fixed over in https://github.com/apollographql/apollo-client/pull/11385

zfogg commented 5 months ago

the mutation issue makes this alpha unusable for me. i'm using my fork still.

wcwung commented 5 months ago

Hey there! We just shipped @apollo/client version 3.9.0-alpha.3, which contains an API that will make this a lot easier:

client.defaultContext

The idea here is that you can modify the defaultContext token of your ApolloClient instance at any time, and that will be available in your link chain.

So here you could do something like this:

// you can optionally enhance the `DefaultContext` like this to add some type safety to it.
declare module '@apollo/client' {
  export interface DefaultContext {
    token?: string
  }
}

function makeClient() {
  const authLink = setContext(async (_, { headers, token }) => {
    return {
      headers: {
        ...headers,
        ...(token ? { authorization: `Bearer ${token}` } : {}),
      },
    };
  });

  return new NextSSRApolloClient({
    cache: new NextSSRInMemoryCache(),
    link: authLink.concat(new HttpLink({ uri: 'https://example.com/graphl' }))
  });
}

export function ApolloWrapper({ children }: { children: React.ReactNode }) {
  return (
    <ApolloNextAppProvider makeClient={makeClient}>
      <UpdateAuth>
        {children}
      </UpdateAuth>
    </ApolloNextAppProvider>
  );
}

function UpdateAuth({ children }: { children: React.ReactNode }) {
  const token = useAuthToken();
  const apolloClient = useApolloClient()
  // just synchronously update the `apolloClient.defaultContext` before any child component can be rendered
  // so the value is available for any query started in a child
  apolloClient.defaultContext.token = token
  return children;
}

This would of course also work for more things than just the token, e.g. for that language selection that @giovannetti-eric is trying to implement here.

It does not solve the problems of accessing cookies in a SSRed client component though - that's something that Next.js has to solve on their side, unfortunately.

Just tried this approach but i'm getting this runtime error: TypeError: Cannot set properties of undefined (setting 'token').

My code for reference:

export const ApolloProvider = ({ children }: IApolloProvider) => {
  return (
    <ApolloNextAppProvider makeClient={makeApolloClient}>
      <UpdateAuth>{children}</UpdateAuth>
    </ApolloNextAppProvider>
  )
}

const UpdateAuth = ({ children }: { children: React.ReactNode }) => {
  const session = getSession()
  const token = session?.token
  const apolloClient = useApolloClient()
  apolloClient.defaultContext.token = token
  return children
}

This is with package versions:

phryneas commented 5 months ago

@wcwung that sounds to me like you might still have an old version of Apollo Client installed, maybe as a dependency of a depenceny. You can do npm ls @apollo/client or yarn why @apollo/client to find out which versions you have installed.

wcwung commented 5 months ago

Thanks! Was able to fix it by setting a resolution:

 "resolutions": {
    "@apollo/client": "^3.9.0-alpha.5"
  },

But I'm now I'm not running into this error: Error: async/await is not yet supported in Client Components, only Server Components. This error is often caused by accidentally adding'use client'to a module that was originally written for the server.

I presume it's the way I'm i'm using async/await to fetch the session and the subsequent token:

 export const UpdateAuth = ({ children }: { children: React.ReactNode }) => {
  const apolloClient = useApolloClient()

  const getToken = async () => {
    const session = await getServerSession(authOptions)
    return session?.token
  }

  return getToken().then((token) => {
    apolloClient.defaultContext.token = token
    return children
  })
}

Is there another suggested approach here when it comes to fetching the token before setting it?

mikew commented 5 months ago

Returning a Promise like your UpdateAuth component does is the same thing as async/await (an async function is just a function that returns a promise). Can you do like was suggested in https://github.com/apollographql/apollo-client-nextjs/issues/103#issuecomment-1790941212 and have your UpdateAuth just take a token? That way, your layouts (which are commonly server components) can do the async/await

// app/layout.tsx
const RootLayout: React.FC = async () => {
  const token = await getServerSession(...)?.token

  return <ApolloNextAppProvider makeClient={makeClient}>
    <UpdateAuth token={token}>...</UpdateAuth>
  </ApolloNextAppProvider>
}

// UpdateAuth.tsx
'use client'

export const UpdateAuth: React.FC<React.PropsWithChildren<{ token?: string }>> = ({ children, token }) => {
  const apolloClient = useApolloClient()
  apolloClient.defaultContext.token = token

  return children
}

p.s I've updated to the latest @apollo/client alpha and the defaultContext is working perfectly in queries and mutations, thanks for looking into that so quickly @phryneas

wcwung commented 5 months ago

@mikew this worked, thank you!

Tushant commented 4 months ago

In my case why am I getting cors issue. Can anyone help me at this please?

import { from } from "@apollo/client";
import { setContext } from "@apollo/client/link/context";
import {
  ApolloNextAppProvider,
  NextSSRApolloClient,
  NextSSRInMemoryCache,
  SSRMultipartLink,
} from "@apollo/experimental-nextjs-app-support/ssr";
import createUploadLink from "apollo-upload-client/createUploadLink.mjs";
import UpdateAuth from "./apollo/UpdateAuth";

function makeClient() {
  const uploadLink = createUploadLink({
    uri: `${process.env.NEXT_PUBLIC_API_URL}`,
    fetchOptions: { cache: "no-store" },
  });

  const authLink = setContext(async (_, context) => {
    console.log("headers", context);
    const modifiedHeader = {
      headers: {
        ...context.headers,
        ...(context.token ? { authorization: `Bearer ${context.token}` } : {}),
      },
    };
    return modifiedHeader;
  });

  const links = [authLink, uploadLink];

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

export function ApolloWrapper({ children, token }) {
  return (
    <ApolloNextAppProvider makeClient={makeClient}>
      <UpdateAuth token={token}>{children}</UpdateAuth>
    </ApolloNextAppProvider>
  );
}

The rsc approach works though

const httpLink = createHttpLink({
  uri: process.env.NEXT_PUBLIC_API_URL,
  fetchOptions: { cache: "no-store" },
});

const authLink = setContext(async (_, { headers }) => {
  const session = await getServerSession(authOptions);
  const modifiedHeader = {
    headers: {
      ...headers,
      authorization: session?.user?.accessToken ? `Bearer ${session?.user?.accessToken}` : ``,
    },
  };
  return modifiedHeader;
});

export const { getClient } = registerApolloClient(() => {
  return new ApolloClient({
    cache: new InMemoryCache(),
    link: from([authLink, httpLink]),
  });
});
phryneas commented 4 months ago

@Tushant a CORS problem usually points to a misconfiguration of your server - I'm sorry, but that's quite out of scope for this library.

Tushant commented 4 months ago

Thanks @phryneas for your reply. The thing is it works in rsc if I use getClient().query({}), however, it does not work if I use query or mutation in the client component.

Actually, I was following this thread for passing token in the headers so I thought to put my problem in the same thread. Sorry to put different issue here.

phryneas commented 4 months ago

@Tushant Yes, because CORS is a browser feature, and if you call getClient().query, that happens on the Next server. This is a misconfiguration of your Graphql server with the result that a user cannot access that server from their browser.

shunshimono commented 4 months ago

Hi there,

I saw the changes in the 3.9.0-alpha.5 release. Will these be part of the stable 3.9.0 release?

Thanks!

phryneas commented 4 months ago

@shunshimono Yes, the prerelease will eventually become the stable release.

indescdevop commented 3 months ago

Hey there! We just shipped @apollo/client version 3.9.0-alpha.3, which contains an API that will make this a lot easier:

client.defaultContext

The idea here is that you can modify the defaultContext token of your ApolloClient instance at any time, and that will be available in your link chain.

So here you could do something like this:

// you can optionally enhance the `DefaultContext` like this to add some type safety to it.
declare module '@apollo/client' {
  export interface DefaultContext {
    token?: string
  }
}

function makeClient() {
  const authLink = setContext(async (_, { headers, token }) => {
    return {
      headers: {
        ...headers,
        ...(token ? { authorization: `Bearer ${token}` } : {}),
      },
    };
  });

  return new NextSSRApolloClient({
    cache: new NextSSRInMemoryCache(),
    link: authLink.concat(new HttpLink({ uri: 'https://example.com/graphl' }))
  });
}

export function ApolloWrapper({ children }: { children: React.ReactNode }) {
  return (
    <ApolloNextAppProvider makeClient={makeClient}>
      <UpdateAuth>
        {children}
      </UpdateAuth>
    </ApolloNextAppProvider>
  );
}

function UpdateAuth({ children }: { children: React.ReactNode }) {
  const token = useAuthToken();
  const apolloClient = useApolloClient()
  // just synchronously update the `apolloClient.defaultContext` before any child component can be rendered
  // so the value is available for any query started in a child
  apolloClient.defaultContext.token = token
  return children;
}

This would of course also work for more things than just the token, e.g. for that language selection that @giovannetti-eric is trying to implement here.

It does not solve the problems of accessing cookies in a SSRed client component though - that's something that Next.js has to solve on their side, unfortunately.

Hello, @phryneas . The code is excellent, but I encountered an error when building the app (also shown in vscode). It states that the return type 'ReactNode' is not a valid JSX element.

using: "@apollo/client": "3.9.0-alpha.5", "@apollo/experimental-nextjs-app-support": "^0.6.0",

Screenshot from 2024-01-31 10-40-27

giovannetti-eric commented 3 months ago

@phryneas

// you can optionally enhance the `DefaultContext` like this to add some type safety to it.
declare module '@apollo/client' {
  export interface DefaultContext {
    token?: string
  }
}

function makeClient() {
  const authLink = setContext(async (_, { headers, token }) => {
    return {
      headers: {
        ...headers,
        ...(token ? { authorization: `Bearer ${token}` } : {}),
      },
    };
  });

  return new NextSSRApolloClient({
    cache: new NextSSRInMemoryCache(),
    link: authLink.concat(new HttpLink({ uri: 'https://example.com/graphl' }))
  });
}

export function ApolloWrapper({ children }: { children: React.ReactNode }) {
  return (
    <ApolloNextAppProvider makeClient={makeClient}>
      <UpdateAuth>
        {children}
      </UpdateAuth>
    </ApolloNextAppProvider>
  );
}

function UpdateAuth({ children }: { children: React.ReactNode }) {
  const token = useAuthToken();
  const apolloClient = useApolloClient()
  // just synchronously update the `apolloClient.defaultContext` before any child component can be rendered
  // so the value is available for any query started in a child
  apolloClient.defaultContext.token = token
  return children;
}

Hi,

Since 3.9 was just released, I tried your suggestion and it worked like a charm, thanks. I don't know if this pattern is somewhere in the doc, but if not, I think it should be added.

Micahnator commented 3 months ago

Thank you all for this thread and the @apollo/client version 3.9.0-alpha.3 update, it was immensely helpful.

Maybe this is a silly question, but where is this useAuthToken() hook coming from?

Is it a custom implementation left to the reader? I don't see it in the MSAL-React documentation (https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-react/docs/hooks.md).

phryneas commented 3 months ago

@indescdevop You can probably also use something like JSX.Element there, or have the wrapper function return <>{children}</> (I've updated the example with this one) instead. That's probably up to your preferences and TypeScript/dependency versions.

phryneas commented 3 months ago

@Micahnator Since there are probably hundred ways of getting authentication tokens from dozens of different system, yes, this is left to the reader, and you'd do what your library provides for that purpose.

skolodyazhnyy commented 1 month ago

Just to share another use case, in my application I need to recreate ApolloClient because user can switch between different instances of GraphQL server. It's a multitenant application and each tenant has own GraphQL endpoint.

So, auth token remains the same, but URL changes. I also need to make sure cache is not shared because schema is the same among all endpoints.

skolodyazhnyy commented 1 month ago

If @phryneas or someone else could give me an advice about my use case ☝️ I would really appreciate it. I managed to make it work by "resetting" singleton instance when user changes tenants but I have a feeling it will eventually break in a bad and unexpected way 😅

phryneas commented 1 month ago

@skolodyazhnyy I think that's the only way you can do this if you actually need to recreate the Client.

The network transport really relies on there being only one instance of Apollo Client on the browser side during rehydration, so we generally need to keep the singleton in place.

Tbh., I'd try to look into different ways than recreating the client, e.g. using a link with directional composition and resetting the cache.

Reckai commented 2 days ago

Hey there! We just shipped @apollo/client version 3.9.0-alpha.3, which contains an API that will make this a lot easier:

client.defaultContext

The idea here is that you can modify the defaultContext token of your ApolloClient instance at any time, and that will be available in your link chain.

So here you could do something like this:

// you can optionally enhance the `DefaultContext` like this to add some type safety to it.
declare module '@apollo/client' {
  export interface DefaultContext {
    token?: string
  }
}

function makeClient() {
  const authLink = setContext(async (_, { headers, token }) => {
    return {
      headers: {
        ...headers,
        ...(token ? { authorization: `Bearer ${token}` } : {}),
      },
    };
  });

  return new NextSSRApolloClient({
    cache: new NextSSRInMemoryCache(),
    link: authLink.concat(new HttpLink({ uri: 'https://example.com/graphl' }))
  });
}

export function ApolloWrapper({ children }: { children: React.ReactNode }) {
  return (
    <ApolloNextAppProvider makeClient={makeClient}>
      <UpdateAuth>
        {children}
      </UpdateAuth>
    </ApolloNextAppProvider>
  );
}

function UpdateAuth({ children }: { children: React.ReactNode }) {
  const token = useAuthToken();
  const apolloClient = useApolloClient()
  // just synchronously update the `apolloClient.defaultContext` before any child component can be rendered
  // so the value is available for any query started in a child
  apolloClient.defaultContext.token = token
  return <>{children}</>;
}

This would of course also work for more things than just the token, e.g. for that language selection that @giovannetti-eric is trying to implement here.

It does not solve the problems of accessing cookies in a SSRed client component though - that's something that Next.js has to solve on their side, unfortunately.

The application's architecture is terrible. How am I supposed to update my cookies, sessions, and so on, if the library's logic does not provide retry on error? When a cookie is updated, I can't let the application know it has been updated. How am I supposed to synchronously update my cookies?