Skip to content

Commit 7838606

Browse files
committed
[compiler] Detect known incompatible libraries (#34027)
A few libraries are known to be incompatible with memoization, whether manually via `useMemo()` or via React Compiler. This puts us in a tricky situation. On the one hand, we understand that these libraries were developed prior to our documenting the [Rules of React](https://react.dev/reference/rules), and their designs were the result of trying to deliver a great experience for their users and balance multiple priorities around DX, performance, etc. At the same time, using these libraries with memoization — and in particular with automatic memoization via React Compiler — can break apps by causing the components using these APIs not to update. Concretely, the APIs have in common that they return a function which returns different values over time, but where the function itself does not change. Memoizing the result on the identity of the function will mean that the value never changes. Developers reasonable interpret this as "React Compiler broke my code". Of course, the best solution is to work with developers of these libraries to address the root cause, and we're doing that. We've previously discussed this situation with both of the respective libraries: * React Hook Form: react-hook-form/react-hook-form#11910 (comment) * TanStack Table: #33057 (comment) and TanStack/table#5567 In the meantime we need to make sure that React Compiler can work out of the box as much as possible. This means teaching it about popular libraries that cannot be memoized. We also can't silently skip compilation, as this confuses users, so we need these error messages to be visible to users. To that end, this PR adds: * A flag to mark functions/hooks as incompatible * Validation against use of such functions * A default type provider to provide declarations for two known-incompatible libraries Note that Mobx is also incompatible, but the `observable()` function is called outside of the component itself, so the compiler cannot currently detect it. We may add validation for such APIs in the future. Again, we really empathize with the developers of these libraries. We've tried to word the error message non-judgementally, because we get that it's hard! We're open to feedback about the error message, please let us know. DiffTrain build for [4082b0e](4082b0e)
1 parent 4a8d929 commit 7838606

35 files changed

+179
-98
lines changed

compiled/eslint-plugin-react-hooks/index.js

Lines changed: 93 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17652,6 +17652,7 @@ var ErrorSeverity;
1765217652
ErrorSeverity["InvalidReact"] = "InvalidReact";
1765317653
ErrorSeverity["InvalidConfig"] = "InvalidConfig";
1765417654
ErrorSeverity["CannotPreserveMemoization"] = "CannotPreserveMemoization";
17655+
ErrorSeverity["IncompatibleLibrary"] = "IncompatibleLibrary";
1765517656
ErrorSeverity["Todo"] = "Todo";
1765617657
ErrorSeverity["Invariant"] = "Invariant";
1765717658
})(ErrorSeverity || (ErrorSeverity = {}));
@@ -17918,7 +17919,8 @@ class CompilerError extends Error {
1791817919
case ErrorSeverity.InvalidJS:
1791917920
case ErrorSeverity.InvalidReact:
1792017921
case ErrorSeverity.InvalidConfig:
17921-
case ErrorSeverity.UnsupportedJS: {
17922+
case ErrorSeverity.UnsupportedJS:
17923+
case ErrorSeverity.IncompatibleLibrary: {
1792217924
return true;
1792317925
}
1792417926
case ErrorSeverity.CannotPreserveMemoization:
@@ -17956,8 +17958,9 @@ function printErrorSummary(severity, message) {
1795617958
severityCategory = 'Error';
1795717959
break;
1795817960
}
17961+
case ErrorSeverity.IncompatibleLibrary:
1795917962
case ErrorSeverity.CannotPreserveMemoization: {
17960-
severityCategory = 'Memoization';
17963+
severityCategory = 'Compilation Skipped';
1796117964
break;
1796217965
}
1796317966
case ErrorSeverity.Invariant: {
@@ -17982,6 +17985,7 @@ var ErrorCategory;
1798217985
ErrorCategory["UseMemo"] = "UseMemo";
1798317986
ErrorCategory["Factories"] = "Factories";
1798417987
ErrorCategory["PreserveManualMemo"] = "PreserveManualMemo";
17988+
ErrorCategory["IncompatibleLibrary"] = "IncompatibleLibrary";
1798517989
ErrorCategory["Immutability"] = "Immutability";
1798617990
ErrorCategory["Globals"] = "Globals";
1798717991
ErrorCategory["Refs"] = "Refs";
@@ -18214,6 +18218,14 @@ function getRuleForCategoryImpl(category) {
1821418218
recommended: true,
1821518219
};
1821618220
}
18221+
case ErrorCategory.IncompatibleLibrary: {
18222+
return {
18223+
category,
18224+
name: 'incompatible-library',
18225+
description: 'Validates against usage of libraries which are incompatible with memoization (manual or automatic)',
18226+
recommended: true,
18227+
};
18228+
}
1821718229
default: {
1821818230
assertExhaustive$1(category, `Unsupported category ${category}`);
1821918231
}
@@ -30841,7 +30853,7 @@ for (const [name, type_] of TYPED_GLOBALS) {
3084130853
DEFAULT_GLOBALS.set('globalThis', addObject(DEFAULT_SHAPES, 'globalThis', TYPED_GLOBALS));
3084230854
DEFAULT_GLOBALS.set('global', addObject(DEFAULT_SHAPES, 'global', TYPED_GLOBALS));
3084330855
function installTypeConfig(globals, shapes, typeConfig, moduleName, loc) {
30844-
var _a, _b, _c, _d;
30856+
var _a, _b, _c, _d, _e, _f;
3084530857
switch (typeConfig.kind) {
3084630858
case 'type': {
3084730859
switch (typeConfig.name) {
@@ -30875,22 +30887,24 @@ function installTypeConfig(globals, shapes, typeConfig, moduleName, loc) {
3087530887
noAlias: typeConfig.noAlias === true,
3087630888
mutableOnlyIfOperandsAreMutable: typeConfig.mutableOnlyIfOperandsAreMutable === true,
3087730889
aliasing: typeConfig.aliasing,
30890+
knownIncompatible: (_a = typeConfig.knownIncompatible) !== null && _a !== void 0 ? _a : null,
3087830891
});
3087930892
}
3088030893
case 'hook': {
3088130894
return addHook(shapes, {
3088230895
hookKind: 'Custom',
30883-
positionalParams: (_a = typeConfig.positionalParams) !== null && _a !== void 0 ? _a : [],
30884-
restParam: (_b = typeConfig.restParam) !== null && _b !== void 0 ? _b : Effect.Freeze,
30896+
positionalParams: (_b = typeConfig.positionalParams) !== null && _b !== void 0 ? _b : [],
30897+
restParam: (_c = typeConfig.restParam) !== null && _c !== void 0 ? _c : Effect.Freeze,
3088530898
calleeEffect: Effect.Read,
3088630899
returnType: installTypeConfig(globals, shapes, typeConfig.returnType, moduleName, loc),
30887-
returnValueKind: (_c = typeConfig.returnValueKind) !== null && _c !== void 0 ? _c : ValueKind.Frozen,
30900+
returnValueKind: (_d = typeConfig.returnValueKind) !== null && _d !== void 0 ? _d : ValueKind.Frozen,
3088830901
noAlias: typeConfig.noAlias === true,
3088930902
aliasing: typeConfig.aliasing,
30903+
knownIncompatible: (_e = typeConfig.knownIncompatible) !== null && _e !== void 0 ? _e : null,
3089030904
});
3089130905
}
3089230906
case 'object': {
30893-
return addObject(shapes, null, Object.entries((_d = typeConfig.properties) !== null && _d !== void 0 ? _d : {}).map(([key, value]) => {
30907+
return addObject(shapes, null, Object.entries((_f = typeConfig.properties) !== null && _f !== void 0 ? _f : {}).map(([key, value]) => {
3089430908
var _a;
3089530909
const type = installTypeConfig(globals, shapes, value, moduleName, loc);
3089630910
const expectHook = isHookName$2(key);
@@ -31091,6 +31105,7 @@ const FunctionTypeSchema = zod.z.object({
3109131105
impure: zod.z.boolean().nullable().optional(),
3109231106
canonicalName: zod.z.string().nullable().optional(),
3109331107
aliasing: AliasingSignatureSchema.nullable().optional(),
31108+
knownIncompatible: zod.z.string().nullable().optional(),
3109431109
});
3109531110
const HookTypeSchema = zod.z.object({
3109631111
kind: zod.z.literal('hook'),
@@ -31100,6 +31115,7 @@ const HookTypeSchema = zod.z.object({
3110031115
returnValueKind: ValueKindSchema.nullable().optional(),
3110131116
noAlias: zod.z.boolean().nullable().optional(),
3110231117
aliasing: AliasingSignatureSchema.nullable().optional(),
31118+
knownIncompatible: zod.z.string().nullable().optional(),
3110331119
});
3110431120
const BuiltInTypeSchema = zod.z.union([
3110531121
zod.z.literal('Any'),
@@ -31601,6 +31617,50 @@ const Resolved = Object.assign(Object.assign({}, Primitives), { nullable(type, p
3160131617
};
3160231618
} });
3160331619

31620+
function defaultModuleTypeProvider(moduleName) {
31621+
switch (moduleName) {
31622+
case 'react-hook-form': {
31623+
return {
31624+
kind: 'object',
31625+
properties: {
31626+
useForm: {
31627+
kind: 'hook',
31628+
returnType: {
31629+
kind: 'object',
31630+
properties: {
31631+
watch: {
31632+
kind: 'function',
31633+
positionalParams: [],
31634+
restParam: Effect.Read,
31635+
calleeEffect: Effect.Read,
31636+
returnType: { kind: 'type', name: 'Any' },
31637+
returnValueKind: ValueKind.Mutable,
31638+
knownIncompatible: `React Hook Form's \`useForm()\` API returns a \`watch()\` function which cannot be memoized safely.`,
31639+
},
31640+
},
31641+
},
31642+
},
31643+
},
31644+
};
31645+
}
31646+
case '@tanstack/react-table': {
31647+
return {
31648+
kind: 'object',
31649+
properties: {
31650+
useReactTable: {
31651+
kind: 'hook',
31652+
positionalParams: [],
31653+
restParam: Effect.Read,
31654+
returnType: { kind: 'type', name: 'Any' },
31655+
knownIncompatible: `TanStack Table's \`useReactTable()\` API returns functions that cannot be memoized safely`,
31656+
},
31657+
},
31658+
};
31659+
}
31660+
}
31661+
return null;
31662+
}
31663+
3160431664
var _Environment_instances, _Environment_globals, _Environment_shapes, _Environment_moduleTypes, _Environment_nextIdentifer, _Environment_nextBlock, _Environment_nextScope, _Environment_scope, _Environment_outlinedFunctions, _Environment_contextIdentifiers, _Environment_hoistedIdentifiers, _Environment_flowTypeEnvironment, _Environment_resolveModuleType, _Environment_isKnownReactModule, _Environment_getCustomHookType;
3160531665
const ReactElementSymbolSchema = zod.z.object({
3160631666
elementSymbol: zod.z.union([
@@ -31963,12 +32023,14 @@ class Environment {
3196332023
}
3196432024
}
3196532025
_Environment_globals = new WeakMap(), _Environment_shapes = new WeakMap(), _Environment_moduleTypes = new WeakMap(), _Environment_nextIdentifer = new WeakMap(), _Environment_nextBlock = new WeakMap(), _Environment_nextScope = new WeakMap(), _Environment_scope = new WeakMap(), _Environment_outlinedFunctions = new WeakMap(), _Environment_contextIdentifiers = new WeakMap(), _Environment_hoistedIdentifiers = new WeakMap(), _Environment_flowTypeEnvironment = new WeakMap(), _Environment_instances = new WeakSet(), _Environment_resolveModuleType = function _Environment_resolveModuleType(moduleName, loc) {
32026+
var _a;
3196632027
let moduleType = __classPrivateFieldGet(this, _Environment_moduleTypes, "f").get(moduleName);
3196732028
if (moduleType === undefined) {
31968-
if (this.config.moduleTypeProvider == null) {
32029+
const moduleTypeProvider = (_a = this.config.moduleTypeProvider) !== null && _a !== void 0 ? _a : defaultModuleTypeProvider;
32030+
if (moduleTypeProvider == null) {
3196932031
return null;
3197032032
}
31971-
const unparsedModuleConfig = this.config.moduleTypeProvider(moduleName);
32033+
const unparsedModuleConfig = moduleTypeProvider(moduleName);
3197232034
if (unparsedModuleConfig != null) {
3197332035
const parsedModuleConfig = TypeSchema.safeParse(unparsedModuleConfig);
3197432036
if (!parsedModuleConfig.success) {
@@ -40776,6 +40838,25 @@ function computeEffectsForLegacySignature(state, signature, lvalue, receiver, ar
4077640838
}),
4077740839
});
4077840840
}
40841+
if (signature.knownIncompatible != null && state.env.isInferredMemoEnabled) {
40842+
const errors = new CompilerError();
40843+
errors.pushDiagnostic(CompilerDiagnostic.create({
40844+
category: ErrorCategory.IncompatibleLibrary,
40845+
severity: ErrorSeverity.IncompatibleLibrary,
40846+
reason: 'Use of incompatible library',
40847+
description: [
40848+
'This API returns functions which cannot be memoized without leading to stale UI. ' +
40849+
'To prevent this, by default React Compiler will skip memoizing this component/hook. ' +
40850+
'However, you may see issues if values from this API are passed to other components/hooks that are ' +
40851+
'memoized.',
40852+
].join(''),
40853+
}).withDetail({
40854+
kind: 'error',
40855+
loc: receiver.loc,
40856+
message: signature.knownIncompatible,
40857+
}));
40858+
throw errors;
40859+
}
4077940860
const stores = [];
4078040861
const captures = [];
4078140862
function visit(place, effect) {
@@ -48253,7 +48334,7 @@ function validateInferredDep(dep, temporaries, declsWithinMemoBlock, validDepsIn
4825348334
errorState.pushDiagnostic(CompilerDiagnostic.create({
4825448335
category: ErrorCategory.PreserveManualMemo,
4825548336
severity: ErrorSeverity.CannotPreserveMemoization,
48256-
reason: 'Compilation skipped because existing memoization could not be preserved',
48337+
reason: 'Existing memoization could not be preserved',
4825748338
description: [
4825848339
'React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. ',
4825948340
'The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. ',
@@ -48414,7 +48495,7 @@ class Visitor extends ReactiveFunctionVisitor {
4841448495
state.errors.pushDiagnostic(CompilerDiagnostic.create({
4841548496
category: ErrorCategory.PreserveManualMemo,
4841648497
severity: ErrorSeverity.CannotPreserveMemoization,
48417-
reason: 'Compilation skipped because existing memoization could not be preserved',
48498+
reason: 'Existing memoization could not be preserved',
4841848499
description: [
4841948500
'React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. ',
4842048501
'This dependency may be mutated later, which could cause the value to change unexpectedly.',
@@ -48451,7 +48532,7 @@ class Visitor extends ReactiveFunctionVisitor {
4845148532
state.errors.pushDiagnostic(CompilerDiagnostic.create({
4845248533
category: ErrorCategory.PreserveManualMemo,
4845348534
severity: ErrorSeverity.CannotPreserveMemoization,
48454-
reason: 'Compilation skipped because existing memoization could not be preserved',
48535+
reason: 'Existing memoization could not be preserved',
4845548536
description: [
4845648537
'React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This value was memoized in source but not in compilation output. ',
4845748538
'',

compiled/facebook-www/REVISION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
8d7b5e490320732f40d9c3aa4590b5b0ae5116f5
1+
4082b0e7d3c042d49ef8987547b923051936956f
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
8d7b5e490320732f40d9c3aa4590b5b0ae5116f5
1+
4082b0e7d3c042d49ef8987547b923051936956f

compiled/facebook-www/React-dev.classic.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1409,7 +1409,7 @@ __DEV__ &&
14091409
exports.useTransition = function () {
14101410
return resolveDispatcher().useTransition();
14111411
};
1412-
exports.version = "19.2.0-www-classic-8d7b5e49-20250827";
1412+
exports.version = "19.2.0-www-classic-4082b0e7-20250828";
14131413
"undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ &&
14141414
"function" ===
14151415
typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop &&

compiled/facebook-www/React-dev.modern.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1409,7 +1409,7 @@ __DEV__ &&
14091409
exports.useTransition = function () {
14101410
return resolveDispatcher().useTransition();
14111411
};
1412-
exports.version = "19.2.0-www-modern-8d7b5e49-20250827";
1412+
exports.version = "19.2.0-www-modern-4082b0e7-20250828";
14131413
"undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ &&
14141414
"function" ===
14151415
typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop &&

compiled/facebook-www/React-prod.classic.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -600,4 +600,4 @@ exports.useSyncExternalStore = function (
600600
exports.useTransition = function () {
601601
return ReactSharedInternals.H.useTransition();
602602
};
603-
exports.version = "19.2.0-www-classic-8d7b5e49-20250827";
603+
exports.version = "19.2.0-www-classic-4082b0e7-20250828";

compiled/facebook-www/React-prod.modern.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -600,4 +600,4 @@ exports.useSyncExternalStore = function (
600600
exports.useTransition = function () {
601601
return ReactSharedInternals.H.useTransition();
602602
};
603-
exports.version = "19.2.0-www-modern-8d7b5e49-20250827";
603+
exports.version = "19.2.0-www-modern-4082b0e7-20250828";

compiled/facebook-www/React-profiling.classic.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -604,7 +604,7 @@ exports.useSyncExternalStore = function (
604604
exports.useTransition = function () {
605605
return ReactSharedInternals.H.useTransition();
606606
};
607-
exports.version = "19.2.0-www-classic-8d7b5e49-20250827";
607+
exports.version = "19.2.0-www-classic-4082b0e7-20250828";
608608
"undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ &&
609609
"function" ===
610610
typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop &&

compiled/facebook-www/React-profiling.modern.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -604,7 +604,7 @@ exports.useSyncExternalStore = function (
604604
exports.useTransition = function () {
605605
return ReactSharedInternals.H.useTransition();
606606
};
607-
exports.version = "19.2.0-www-modern-8d7b5e49-20250827";
607+
exports.version = "19.2.0-www-modern-4082b0e7-20250828";
608608
"undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ &&
609609
"function" ===
610610
typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop &&

compiled/facebook-www/ReactART-dev.classic.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19588,10 +19588,10 @@ __DEV__ &&
1958819588
(function () {
1958919589
var internals = {
1959019590
bundleType: 1,
19591-
version: "19.2.0-www-classic-8d7b5e49-20250827",
19591+
version: "19.2.0-www-classic-4082b0e7-20250828",
1959219592
rendererPackageName: "react-art",
1959319593
currentDispatcherRef: ReactSharedInternals,
19594-
reconcilerVersion: "19.2.0-www-classic-8d7b5e49-20250827"
19594+
reconcilerVersion: "19.2.0-www-classic-4082b0e7-20250828"
1959519595
};
1959619596
internals.overrideHookState = overrideHookState;
1959719597
internals.overrideHookStateDeletePath = overrideHookStateDeletePath;
@@ -19625,7 +19625,7 @@ __DEV__ &&
1962519625
exports.Shape = Shape;
1962619626
exports.Surface = Surface;
1962719627
exports.Text = Text;
19628-
exports.version = "19.2.0-www-classic-8d7b5e49-20250827";
19628+
exports.version = "19.2.0-www-classic-4082b0e7-20250828";
1962919629
"undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ &&
1963019630
"function" ===
1963119631
typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop &&

0 commit comments

Comments
 (0)