Skip to content

Commit d98b87b

Browse files
0.10.0 (2) - Warn if makeClient returns the same client for multiple requests. (#205)
* Warn if makeClient returns the same client for multiple requests. * Add test * Apply suggestions from code review Co-authored-by: Jerel Miller <[email protected]> --------- Co-authored-by: Jerel Miller <[email protected]>
1 parent e0b4862 commit d98b87b

File tree

2 files changed

+106
-5
lines changed

2 files changed

+106
-5
lines changed

packages/client-react-streaming/src/registerApolloClient.test.tsx

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable no-inner-declarations */
12
import { it } from "node:test";
23
import assert from "node:assert";
34
import { runInConditions } from "./util/runInConditions.js";
@@ -129,3 +130,66 @@ it("calling `getClient` with parameters results in an error", async () => {
129130
/You cannot pass arguments into `getClient`./.test(error?.message || "")
130131
);
131132
});
133+
134+
it("warns if `makeClient` calls that should return different `ApolloClient` instances return the same one (outside of React)", () => {
135+
const warn = console.warn;
136+
try {
137+
let warnArgs: unknown[] | undefined = undefined;
138+
console.warn = (...args) => (warnArgs = args);
139+
const same = makeClient();
140+
const { getClient } = registerApolloClient(() => same);
141+
142+
getClient();
143+
// `console.warn` has not been called
144+
assert.equal(warnArgs, undefined);
145+
146+
getClient();
147+
// `console.warn` has been called
148+
assert.ok(
149+
/Multiple calls to `getClient` for different requests returned the same client instance./i.test(
150+
String(warnArgs![0])
151+
)
152+
);
153+
} finally {
154+
console.warn = warn;
155+
}
156+
});
157+
158+
it("warns if `makeClient` calls that should return different `ApolloClient` instances return the same one (in React)", async () => {
159+
const warn = console.warn;
160+
try {
161+
let warnArgs: unknown[] | undefined = undefined;
162+
console.warn = (...args) => (warnArgs = args);
163+
const same = makeClient();
164+
const { getClient } = registerApolloClient(() => same);
165+
166+
function App() {
167+
getClient();
168+
// it should be perfectly fine and not cause any errors to call `getClient` in here as often as we want to
169+
getClient();
170+
getClient();
171+
return <div></div>;
172+
}
173+
174+
{
175+
const stream = renderToPipeableStream(React.createElement(App), {});
176+
await drain(stream);
177+
}
178+
// we had only one request, `console.warn` has not been called
179+
assert.equal(warnArgs, undefined);
180+
181+
{
182+
// second render - different request, but returns `same` ApolloClient as before
183+
const stream = renderToPipeableStream(React.createElement(App), {});
184+
await drain(stream);
185+
}
186+
187+
assert.ok(
188+
/Multiple calls to `getClient` for different requests returned the same client instance./i.test(
189+
String(warnArgs![0])
190+
)
191+
);
192+
} finally {
193+
console.warn = warn;
194+
}
195+
});

packages/client-react-streaming/src/registerApolloClient.tsx

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
import type { ApolloClient } from "@apollo/client/index.js";
22
import { cache } from "react";
33

4+
const seenWrappers = WeakSet
5+
? new WeakSet<{ client: ApolloClient<any> | Promise<ApolloClient<any>> }>()
6+
: undefined;
7+
const seenClients = WeakSet
8+
? new WeakSet<ApolloClient<any> | Promise<ApolloClient<any>>>()
9+
: undefined;
10+
411
/**
512
* > This export is only available in React Server Components
613
*
7-
* Ensures that you can always access the same instance of ApolloClient
14+
* Ensures that you can always access the same instance of ApolloClient
815
* during RSC for an ongoing request, while always returning
916
* a new instance for different requests.
1017
*
@@ -26,7 +33,7 @@ export function registerApolloClient(
2633
makeClient: () => Promise<ApolloClient<any>>
2734
): { getClient: () => Promise<ApolloClient<any>> };
2835
/**
29-
* Ensures that you can always access the same instance of ApolloClient
36+
* Ensures that you can always access the same instance of ApolloClient
3037
* during RSC for an ongoing request, while always returning
3138
* a new instance for different requests.
3239
*
@@ -50,7 +57,19 @@ export function registerApolloClient(makeClient: () => ApolloClient<any>): {
5057
export function registerApolloClient(
5158
makeClient: (() => Promise<ApolloClient<any>>) | (() => ApolloClient<any>)
5259
) {
53-
function wrappedMakeClient() {
60+
// React invalidates the cache on each server request, so the wrapping
61+
// object is needed to properly detect whether the client is a unique
62+
// reference or not. We can warn if `cachedMakeWrappedClient` creates a new "wrapper",
63+
// but with a `client` property that we have already seen before.
64+
// In that case, not every call to `makeClient` would create a new
65+
// `ApolloClient` instance.
66+
function makeWrappedClient() {
67+
return { client: makeClient() };
68+
}
69+
70+
const cachedMakeWrappedClient = cache(makeWrappedClient);
71+
72+
function getClient() {
5473
if (arguments.length) {
5574
throw new Error(
5675
`
@@ -61,9 +80,27 @@ resulting in duplicate requests and a non-functional cache.
6180
`.trim()
6281
);
6382
}
64-
return makeClient();
83+
const wrapper = cachedMakeWrappedClient();
84+
if (seenWrappers && seenClients) {
85+
if (!seenWrappers.has(wrapper)) {
86+
if (seenClients.has(wrapper.client)) {
87+
console.warn(
88+
`
89+
Multiple calls to \`getClient\` for different requests returned the same client instance.
90+
This means that private user data could accidentally be shared between requests.
91+
This happens, for example, if you create a global \`ApolloClient\` instance and your \`makeClient\`
92+
implementation just looks like \`() => client\`.
93+
Always call \`new ApolloClient\` **inside** your \`makeClient\` function and
94+
return a new instance every time \`makeClient\` is called.
95+
`.trim()
96+
);
97+
}
98+
seenWrappers.add(wrapper);
99+
seenClients.add(wrapper.client);
100+
}
101+
}
102+
return wrapper.client;
65103
}
66-
const getClient = cache(wrappedMakeClient);
67104
return {
68105
getClient,
69106
};

0 commit comments

Comments
 (0)