Skip to content

Commit 7fb7939

Browse files
authored
switch useRenderGuard to an approach not accessing React's internals (#11888)
1 parent 6536369 commit 7fb7939

File tree

4 files changed

+45
-61
lines changed

4 files changed

+45
-61
lines changed

.changeset/brown-bikes-divide.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@apollo/client": patch
3+
---
4+
5+
switch `useRenderGuard` to an approach not accessing React's internals

.size-limits.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
{
2-
"dist/apollo-client.min.cjs": 39620,
2+
"dist/apollo-client.min.cjs": 39561,
33
"import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32821
44
}

src/react/hooks/internal/__tests__/useRenderGuard.test.tsx

-43
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import React, { useEffect } from "rehackt";
33
import { useRenderGuard } from "../useRenderGuard";
44
import { render, waitFor } from "@testing-library/react";
5-
import { withCleanup } from "../../../../testing/internal";
65

76
const UNDEF = {};
87
const IS_REACT_19 = React.version.startsWith("19");
@@ -35,45 +34,3 @@ it("returns a function that returns `false` if called after render", async () =>
3534
});
3635
expect(result).toBe(false);
3736
});
38-
39-
function breakReactInternalsTemporarily() {
40-
const R = React as unknown as {
41-
__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: any;
42-
};
43-
const orig = R.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;
44-
45-
R.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = {};
46-
return withCleanup({}, () => {
47-
R.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = orig;
48-
});
49-
}
50-
51-
it("results in false negatives if React internals change", () => {
52-
let result: boolean | typeof UNDEF = UNDEF;
53-
function TestComponent() {
54-
using _ = breakReactInternalsTemporarily();
55-
const calledDuringRender = useRenderGuard();
56-
result = calledDuringRender();
57-
return <>Test</>;
58-
}
59-
render(<TestComponent />);
60-
expect(result).toBe(false);
61-
});
62-
63-
it("does not result in false positives if React internals change", async () => {
64-
let result: boolean | typeof UNDEF = UNDEF;
65-
function TestComponent() {
66-
using _ = breakReactInternalsTemporarily();
67-
const calledDuringRender = useRenderGuard();
68-
useEffect(() => {
69-
using _ = breakReactInternalsTemporarily();
70-
result = calledDuringRender();
71-
});
72-
return <>Test</>;
73-
}
74-
render(<TestComponent />);
75-
await waitFor(() => {
76-
expect(result).not.toBe(UNDEF);
77-
});
78-
expect(result).toBe(false);
79-
});
+39-17
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,45 @@
11
import * as React from "rehackt";
22

3-
function getRenderDispatcher() {
4-
return (React as any).__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
5-
?.ReactCurrentDispatcher?.current;
6-
}
7-
8-
let RenderDispatcher: unknown = null;
3+
let Ctx: React.Context<null>;
94

10-
/*
11-
Relay does this too, so we hope this is safe.
12-
https://github.com/facebook/relay/blob/8651fbca19adbfbb79af7a3bc40834d105fd7747/packages/react-relay/relay-hooks/loadQuery.js#L90-L98
13-
*/
5+
function noop() {}
146
export function useRenderGuard() {
15-
// eslint-disable-next-line react-compiler/react-compiler
16-
RenderDispatcher = getRenderDispatcher();
7+
if (!Ctx) {
8+
// we want the intialization to be lazy because `createContext` would error on import in a RSC
9+
Ctx = React.createContext(null);
10+
}
11+
12+
return React.useCallback(
13+
/**
14+
* @returns true if the hook was called during render
15+
*/ () => {
16+
const orig = console.error;
17+
try {
18+
console.error = noop;
1719

18-
return React.useCallback(() => {
19-
return (
20-
RenderDispatcher != null && RenderDispatcher === getRenderDispatcher()
21-
);
22-
}, []);
20+
/**
21+
* `useContext` can be called conditionally during render, so this is safe.
22+
* (Also, during render we would want to throw as a reaction to this anyways, so it
23+
* wouldn't even matter if we got the order of hooks mixed up...)
24+
*
25+
* They cannot however be called outside of Render, and that's what we're testing here.
26+
*
27+
* Different versions of React have different behaviour on an invalid hook call:
28+
*
29+
* React 16.8 - 17: throws an error
30+
* https://github.com/facebook/react/blob/2b93d686e359c7afa299e2ec5cf63160a32a1155/packages/react/src/ReactHooks.js#L18-L26
31+
*
32+
* React 18 & 19: `console.error` in development, then `resolveDispatcher` returns `null` and a member access on `null` throws.
33+
* https://github.com/facebook/react/blob/58e8304483ebfadd02a295339b5e9a989ac98c6e/packages/react/src/ReactHooks.js#L28-L35
34+
*/
35+
React["useContext" /* hide this from the linter */](Ctx);
36+
return true;
37+
} catch (e) {
38+
return false;
39+
} finally {
40+
console.error = orig;
41+
}
42+
},
43+
[]
44+
);
2345
}

0 commit comments

Comments
 (0)