-
Notifications
You must be signed in to change notification settings - Fork 36
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
Comments
i actually solve this here: 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 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. 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 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 |
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 But of course, changing the |
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 |
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 |
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 |
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 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!
|
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 |
@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 |
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 apollographql/apollo-client#11385 |
the mutation issue makes this alpha unusable for me. i'm using my fork still. |
Just tried this approach but i'm getting this runtime error: 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. |
Thanks! Was able to fix it by setting a resolution:
But I'm now I'm not running into this error: I presume it's the way I'm i'm using 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 // 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 |
@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 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 |
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. |
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: |
// 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. |
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 |
@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. |
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 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. If this approach doesn't work for you, maybe implementing a custom intermediate 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. |
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 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 Thanks for you time. |
@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. 😅 |
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 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 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}</>;
} |
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, |
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?The text was updated successfully, but these errors were encountered: