diff --git a/.changeset/fix-950-incompatible-library-copy.md b/.changeset/fix-950-incompatible-library-copy.md new file mode 100644 index 000000000..f9ec3c571 --- /dev/null +++ b/.changeset/fix-950-incompatible-library-copy.md @@ -0,0 +1,18 @@ +--- +"@react-doctor/core": patch +--- + +Fix misleading remediation for react-hooks-js/incompatible-library + +`react-hooks-js/incompatible-library` fires when the React Compiler can't +memoize through a third-party hook (e.g. `@tanstack/react-virtual`'s +`useVirtualizer`). The diagnostic carried the generic React Compiler action — +"Rewrite the flagged code so the compiler can optimize it" — which reads as +"reimplement the library locally" and steered users off mature libraries. + +The rule stays active (the compiler's own bail-out reason is informative), but +its remediation now names the real fix: it's how the library works, not a bug in +your code — memoize values you pass from it into other memoized components, or +suppress it with `// react-doctor-disable-next-line react-hooks-js/incompatible-library`. + +Closes #950 diff --git a/packages/core/src/runners/oxlint/parse-output.ts b/packages/core/src/runners/oxlint/parse-output.ts index a1285d78c..c6b21a3aa 100644 --- a/packages/core/src/runners/oxlint/parse-output.ts +++ b/packages/core/src/runners/oxlint/parse-output.ts @@ -34,12 +34,21 @@ const REACT_COMPILER_TODO_TITLE = "React Compiler doesn't support this syntax"; const REACT_COMPILER_IMPACT = "This component misses React Compiler's automatic memoization & re-renders more than it should"; const REACT_COMPILER_ACTION = "Rewrite the flagged code so the compiler can optimize it."; +// `incompatible-library` fires on a third-party hook the compiler can't memoize +// through (e.g. @tanstack/react-virtual's `useVirtualizer`) — code the user +// can't and shouldn't rewrite. The generic "rewrite it" action wrongly steers +// users off mature libraries (#950), so this rule names the real fix instead. +const REACT_COMPILER_INCOMPATIBLE_LIBRARY_ACTION = + "It's how the library works, not a bug in your code. Memoize values you pass from it into other memoized components, or suppress it with `// react-doctor-disable-next-line react-hooks-js/incompatible-library`."; const REACT_COMPILER_GENERIC_MESSAGE = `${REACT_COMPILER_IMPACT}. ${REACT_COMPILER_ACTION}`; -const buildReactCompilerMessage = (reasonSummary: string): string => { +const buildReactCompilerMessage = ( + reasonSummary: string, + action = REACT_COMPILER_ACTION, +): string => { const normalizedSummary = reasonSummary.replace(TRAILING_PERIOD_PATTERN, ""); - if (!normalizedSummary) return REACT_COMPILER_GENERIC_MESSAGE; - return `${REACT_COMPILER_IMPACT}: ${normalizedSummary}. ${REACT_COMPILER_ACTION}`; + if (!normalizedSummary) return `${REACT_COMPILER_IMPACT}. ${action}`; + return `${REACT_COMPILER_IMPACT}: ${normalizedSummary}. ${action}`; }; // Adopted third-party plugins (not in the react-doctor registry) → the @@ -167,7 +176,12 @@ const resolveCleanedDiagnostic = ( const [reasonSummary = "", ...reasonDetailLines] = bailoutReason.split("\n"); const reasonDetail = reasonDetailLines.join("\n").trim(); return { - message: buildReactCompilerMessage(reasonSummary.trim()), + message: buildReactCompilerMessage( + reasonSummary.trim(), + rule === "incompatible-library" + ? REACT_COMPILER_INCOMPATIBLE_LIBRARY_ACTION + : REACT_COMPILER_ACTION, + ), help: appendReanimatedSharedValueHint(reasonDetail || help, rule, project), }; } diff --git a/packages/core/tests/react-compiler-bailout-message.test.ts b/packages/core/tests/react-compiler-bailout-message.test.ts index af8681d9a..4e3cd8428 100644 --- a/packages/core/tests/react-compiler-bailout-message.test.ts +++ b/packages/core/tests/react-compiler-bailout-message.test.ts @@ -74,4 +74,18 @@ describe("parseOxlintOutput react-hooks-js bail-out reason in primary message", expect(diagnostic.message).toContain(": This value is impure. Rewrite"); }); + + it("gives incompatible-library a library-specific action, not the generic 'rewrite it' copy", () => { + const reason = + "This API returns functions which cannot be memoized without leading to stale UI"; + const stdout = buildOxlintStdout("react-hooks-js(incompatible-library)", reason); + const [diagnostic] = parseOxlintOutput(stdout, buildProject(), TEST_ROOT_DIRECTORY); + + expect(diagnostic.message).toContain(reason); + expect(diagnostic.message).toContain("not a bug in your code"); + expect(diagnostic.message).toContain( + "react-doctor-disable-next-line react-hooks-js/incompatible-library", + ); + expect(diagnostic.message).not.toContain("Rewrite the flagged code"); + }); });