Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How do i re-create the client with <ApolloNextAppProvider />? #103

Open
zfogg opened this issue Sep 29, 2023 · 52 comments
Open

How do i re-create the client with <ApolloNextAppProvider />? #103

zfogg opened this issue Sep 29, 2023 · 52 comments

Comments

@zfogg
Copy link

zfogg commented Sep 29, 2023

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 zfogg changed the title How do i re-create the client with <ApolloNextProvider />? How do i re-create the client with <ApolloNextAppProvider />? Sep 29, 2023
@zfogg
Copy link
Author

zfogg commented Sep 29, 2023

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
Copy link
Member

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
Copy link
Author

zfogg commented Sep 29, 2023

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
Copy link
Author

zfogg commented Sep 29, 2023

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
Copy link
Member

phryneas commented Sep 29, 2023

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
Copy link
Member

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
Copy link
Author

zfogg commented Sep 29, 2023

i'll look into this! thanks

@zfogg
Copy link
Author

zfogg commented Sep 29, 2023

will close the issue if it works

@zfogg
Copy link
Author

zfogg commented Sep 29, 2023

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
Copy link
Member

phryneas commented Oct 2, 2023

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
Copy link
Author

zfogg commented Oct 3, 2023

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
Copy link
Author

zfogg commented Oct 3, 2023

so i need makeClient to run again

@zfogg
Copy link
Author

zfogg commented Oct 3, 2023

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
Copy link

mvandergrift commented Oct 3, 2023

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
Copy link
Member

phryneas commented Oct 3, 2023

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
Copy link

Sashkan commented Oct 31, 2023

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
Copy link
Member

phryneas commented Oct 31, 2023

@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
Copy link

giovannetti-eric commented Nov 1, 2023

Hello there!

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

I'm on the latest version of each packages:

  • "@apollo/client": "^3.8.6",
  • "@apollo/experimental-nextjs-app-support": "^0.5.0",
  • "next": "^14.0.0",

I have 2 things to solve:

  • Updating the Accept-Language header on language change
  • Updating the bearer authorization token on sign-in

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
Copy link
Member

phryneas commented Nov 2, 2023

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
Copy link
Author

zfogg commented Nov 3, 2023

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
Copy link
Member

phryneas commented Nov 3, 2023

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

@Sashkan
Copy link

Sashkan commented Nov 6, 2023

@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
Copy link
Member

phryneas commented Nov 6, 2023

@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
Copy link
Author

zfogg commented Nov 7, 2023

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
Copy link

mikew commented Nov 22, 2023

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
Copy link
Member

@mikew thank you for the report - that will be fixed over in apollographql/apollo-client#11385

@zfogg
Copy link
Author

zfogg commented Nov 28, 2023

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

@wcwung
Copy link

wcwung commented Dec 7, 2023

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:

  • @apollo/client v3.9.0-alpha.5
  • @apollo/experimental-nextjs-app-support v0.5.2.

@phryneas
Copy link
Member

phryneas commented Dec 7, 2023

@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
Copy link

wcwung commented Dec 8, 2023

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
Copy link

mikew commented Dec 8, 2023

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 #103 (comment) 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
Copy link

wcwung commented Dec 8, 2023

@mikew this worked, thank you!

@Tushant
Copy link

Tushant commented Dec 21, 2023

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
Copy link
Member

@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
Copy link

Tushant commented Dec 21, 2023

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
Copy link
Member

phryneas commented Dec 21, 2023

@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
Copy link

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
Copy link
Member

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

@indescdevop
Copy link

indescdevop commented Jan 31, 2024

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
Copy link

@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
Copy link

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
Copy link
Member

phryneas commented Feb 1, 2024

@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
Copy link
Member

phryneas commented Feb 1, 2024

@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
Copy link

skolodyazhnyy commented Mar 12, 2024

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
Copy link

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
Copy link
Member

@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
Copy link

Reckai commented May 8, 2024

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?

@phryneas
Copy link
Member

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?

@Reckai please note that this approach has been added to help with situations where you use an external library like next-auth that handles the full lifecycle of your token for you, but only exposes it through a React hook such as useSession. It's not meant to be the be-all-end-all way of authentication.

Unfortunately, there are hundreds of ways of doing authentication, from hand-written tokens in localStorage in headers over cookies that are set by the server, to third-party libraries that only expose few parts of their workflow in sometimes obscure ways. Auth can happen via plain HTTP, GraphQL or even WebSocket communication.
We cannot provide a one-fits-all solution for all that.
We can only add more places you can hook into.

If this approach doesn't work for you, maybe implementing a custom intermediate ApolloLink can.

I am currently in the process of writing more documentation for this package, and in that process I'm also planning to show a few more different examples in our authentication docs - but because of the complexity of the topic I fear we will never be able to cover all ways of managing authenticating. It's just too diverse as a topic.

@giovannetti-eric
Copy link

Hi @phryneas, I have just catched today that the client doesn't seems to update anymore with this technique, I'm not sure what caused the regression, and I have updated the packages few days ago, but maybe the problem was here for a longer time.

The token variable in UpdateAuth is updated, it trigger setContext, but the token value in setContext is the previous one and not the new one, so the client is not updated.

I checked a couple of hours on my side, but didn't found any anomaly on my side.

Did you know if the recent releases of @apollo/client or @apollo/experimental-nextjs-app-support could be the cause of a regression on that (there was a lot of change recently, but I'm not sure if it can have in impact on this part)?

Thanks for you time.

@phryneas
Copy link
Member

@giovannetti-eric there are about 5 different techniques in this issue shared by different people over time, to I can't really tell what you are currently doing. 😅
Could you please open an individual issue and share the code you are currently using there?

@samchungy
Copy link

samchungy commented Oct 20, 2024

G'day @phryneas, is there a nice way to handle clearing the client's store with this defaultContext approach of setting an auth token? As an example, when I have a different user so I want to make sure the store is clear.

eg.

function UpdateAuth({ children }: { children: React.ReactNode }) {
  const {user, token} = useAuthToken();
  const previousUser = React.useRef(user);
  const [clearing, setClearing] = useState(false);
  const apolloClient = useApolloClient();
  
  if (user !== previousUser.current) {
  	previousUser.current = user;
    apolloClient.clearStore();
  }
  
  return <>{children}</>;
}

I found if my children used useQuery, the clearStore would break and cancel the current operation.

I'm currently using the following to work around this issue:

function UpdateAuth({ children }: { children: React.ReactNode }) {
  const {user, token} = useAuthToken();
  const previousUser = React.useRef(user);
  const [clearing, setClearing] = useState(false);
  const apolloClient = useApolloClient();
  
  if (user !== previousUser.current) {
    previousUser.current = user;
    setClearing(true);
    apolloClient.clearStore().then(() => setClearing(false))
  }
  
  if (clearing) {
    return null;
  }
  
  apolloClient.defaultContext.token = token
  
  return <>{children}</>;
}

EDIT: actually resetStore seems to be the right method for me but I'm not sure I like having to handle an invariant error in the code :\

function UpdateAuth({ children }: { children: React.ReactNode }) {
  const {user, token} = useAuthToken();
  const previousUser = React.useRef(user);
  const [clearing, setClearing] = useState(false);
  const apolloClient = useApolloClient();
  
  if (user !== previousUser.current) {
  	previousUser.current = user;
    apolloClient.resetStore();
  }
  
  return <>{children}</>;
}

@phryneas
Copy link
Member

phryneas commented Oct 21, 2024

I found if my children used useQuery, the clearStore would break and cancel the current operation.

That's kinda what it should do, though, since that last operation was made with now-outdated credentials, right?

Generally, we had a bug in the past that this would error in the wrong place - I recommend updating Apollo Client, you might find the behaviour of AC more in line with your expectations now.

That said, resetStore also calls clearStore, and is probably more what you need here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests