Open zfogg opened 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?
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?
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
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
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.
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>
}
i'll look into this! thanks
will close the issue if it works
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?
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.
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
so i need makeClient to run again
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
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()
})
);
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.
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.
@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.
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?
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.
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 😄
Let's leave this open for visibility for now - it seems quite a lot of people are landing here :)
@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 🤔
@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.
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
Hey there! We just shipped
@apollo/client
version3.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 yourApolloClient
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]),
})
}
@mikew thank you for the report - that will be fixed over in https://github.com/apollographql/apollo-client/pull/11385
the mutation issue makes this alpha unusable for me. i'm using my fork still.
Hey there! We just shipped
@apollo/client
version3.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 yourApolloClient
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:
@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.
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?
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
@mikew this worked, thank you!
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]),
});
});
@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.
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.
@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.
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!
@shunshimono Yes, the prerelease will eventually become the stable release.
Hey there! We just shipped
@apollo/client
version3.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 yourApolloClient
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",
@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.
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).
@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.
@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.
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.
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 😅
@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.
Hey there! We just shipped
@apollo/client
version3.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 yourApolloClient
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?
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?