diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts
index 2b007f9659b99..4134fadf1d7d7 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts
+++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts
@@ -33,6 +33,7 @@ import {
Type,
ValidatedIdentifier,
ValueKind,
+ getHookKindForType,
makeBlockId,
makeIdentifierId,
makeIdentifierName,
@@ -788,14 +789,9 @@ export class Environment {
);
} else {
const moduleType = this.#resolveModuleType(binding.module, loc);
+ let propertyType: Type | null = null;
if (moduleType !== null) {
- const importedType = this.getPropertyType(
- moduleType,
- binding.imported,
- );
- if (importedType != null) {
- return importedType;
- }
+ propertyType = this.getPropertyType(moduleType, binding.imported);
}
/**
@@ -806,9 +802,18 @@ export class Environment {
* `import {useHook as foo} ...`
* `import {foo as useHook} ...`
*/
- return isHookName(binding.imported) || isHookName(binding.name)
- ? this.#getCustomHookType()
- : null;
+ const expectHook =
+ isHookName(binding.imported) || isHookName(binding.name);
+ if (expectHook) {
+ if (
+ propertyType &&
+ getHookKindForType(this, propertyType) !== null
+ ) {
+ return propertyType;
+ }
+ return this.#getCustomHookType();
+ }
+ return propertyType;
}
}
case 'ImportDefault':
@@ -821,17 +826,27 @@ export class Environment {
);
} else {
const moduleType = this.#resolveModuleType(binding.module, loc);
+ let importedType: Type | null = null;
if (moduleType !== null) {
if (binding.kind === 'ImportDefault') {
const defaultType = this.getPropertyType(moduleType, 'default');
if (defaultType !== null) {
- return defaultType;
+ importedType = defaultType;
}
} else {
- return moduleType;
+ importedType = moduleType;
+ }
+ }
+ if (isHookName(binding.name)) {
+ if (
+ importedType !== null &&
+ getHookKindForType(this, importedType) !== null
+ ) {
+ return importedType;
}
+ return this.#getCustomHookType();
}
- return isHookName(binding.name) ? this.#getCustomHookType() : null;
+ return importedType;
}
}
}
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-hooklike-name-not-typed-as-hook-import.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-hooklike-name-not-typed-as-hook-import.expect.md
new file mode 100644
index 0000000000000..297056ffb7780
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-hooklike-name-not-typed-as-hook-import.expect.md
@@ -0,0 +1,123 @@
+
+## Input
+
+```javascript
+import {useMemo} from 'react';
+import {
+ useArrayConcatNotTypedAsHook,
+ ValidateMemoization,
+} from 'shared-runtime';
+
+export function Component({a, b}) {
+ const item1 = useMemo(() => [a], [a]);
+ const item2 = useMemo(() => [b], [b]);
+ const item3 = useArrayConcatNotTypedAsHook(item1, item2);
+
+ return (
+ <>
+
+ >
+ );
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: Component,
+ params: [{a: 0, b: 0}],
+ sequentialRenders: [
+ {a: 0, b: 0},
+ {a: 1, b: 0},
+ {a: 1, b: 1},
+ {a: 1, b: 2},
+ {a: 2, b: 2},
+ {a: 3, b: 2},
+ {a: 0, b: 0},
+ ],
+};
+
+```
+
+## Code
+
+```javascript
+import { c as _c } from "react/compiler-runtime";
+import { useMemo } from "react";
+import {
+ useArrayConcatNotTypedAsHook,
+ ValidateMemoization,
+} from "shared-runtime";
+
+export function Component(t0) {
+ const $ = _c(10);
+ const { a, b } = t0;
+ let t1;
+ let t2;
+ if ($[0] !== a) {
+ t2 = [a];
+ $[0] = a;
+ $[1] = t2;
+ } else {
+ t2 = $[1];
+ }
+ t1 = t2;
+ const item1 = t1;
+ let t3;
+ let t4;
+ if ($[2] !== b) {
+ t4 = [b];
+ $[2] = b;
+ $[3] = t4;
+ } else {
+ t4 = $[3];
+ }
+ t3 = t4;
+ const item2 = t3;
+ const item3 = useArrayConcatNotTypedAsHook(item1, item2);
+ let t5;
+ if ($[4] !== a || $[5] !== b) {
+ t5 = [a, b];
+ $[4] = a;
+ $[5] = b;
+ $[6] = t5;
+ } else {
+ t5 = $[6];
+ }
+ let t6;
+ if ($[7] !== t5 || $[8] !== item3) {
+ t6 = (
+ <>
+
+ >
+ );
+ $[7] = t5;
+ $[8] = item3;
+ $[9] = t6;
+ } else {
+ t6 = $[9];
+ }
+ return t6;
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: Component,
+ params: [{ a: 0, b: 0 }],
+ sequentialRenders: [
+ { a: 0, b: 0 },
+ { a: 1, b: 0 },
+ { a: 1, b: 1 },
+ { a: 1, b: 2 },
+ { a: 2, b: 2 },
+ { a: 3, b: 2 },
+ { a: 0, b: 0 },
+ ],
+};
+
+```
+
+### Eval output
+(kind: ok)
{"inputs":[0,0],"output":[0,0]}
+{"inputs":[1,0],"output":[1,0]}
+{"inputs":[1,1],"output":[1,1]}
+{"inputs":[1,2],"output":[1,2]}
+{"inputs":[2,2],"output":[2,2]}
+{"inputs":[3,2],"output":[3,2]}
+{"inputs":[0,0],"output":[0,0]}
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-hooklike-name-not-typed-as-hook-import.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-hooklike-name-not-typed-as-hook-import.tsx
new file mode 100644
index 0000000000000..8cd065b228fc7
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-hooklike-name-not-typed-as-hook-import.tsx
@@ -0,0 +1,31 @@
+import {useMemo} from 'react';
+import {
+ useArrayConcatNotTypedAsHook,
+ ValidateMemoization,
+} from 'shared-runtime';
+
+export function Component({a, b}) {
+ const item1 = useMemo(() => [a], [a]);
+ const item2 = useMemo(() => [b], [b]);
+ const item3 = useArrayConcatNotTypedAsHook(item1, item2);
+
+ return (
+ <>
+
+ >
+ );
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: Component,
+ params: [{a: 0, b: 0}],
+ sequentialRenders: [
+ {a: 0, b: 0},
+ {a: 1, b: 0},
+ {a: 1, b: 1},
+ {a: 1, b: 2},
+ {a: 2, b: 2},
+ {a: 3, b: 2},
+ {a: 0, b: 0},
+ ],
+};
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-import-object-typed-module-as-hook-name.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-import-object-typed-module-as-hook-name.expect.md
new file mode 100644
index 0000000000000..7ef2ee7e26fb9
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-import-object-typed-module-as-hook-name.expect.md
@@ -0,0 +1,145 @@
+
+## Input
+
+```javascript
+import {useMemo} from 'react';
+import useHook from 'shared-runtime';
+
+export function Component({a, b}) {
+ const item1 = useMemo(() => ({a}), [a]);
+ const item2 = useMemo(() => ({b}), [b]);
+ useHook(item1, item2);
+
+ return (
+ <>
+
+
+ >
+ );
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: Component,
+ params: [{a: 0, b: 0}],
+ sequentialRenders: [
+ {a: 0, b: 0},
+ {a: 1, b: 0},
+ {a: 1, b: 1},
+ {a: 1, b: 2},
+ {a: 2, b: 2},
+ {a: 3, b: 2},
+ {a: 0, b: 0},
+ ],
+};
+
+```
+
+## Code
+
+```javascript
+import { c as _c } from "react/compiler-runtime";
+import { useMemo } from "react";
+import useHook from "shared-runtime";
+
+export function Component(t0) {
+ const $ = _c(17);
+ const { a, b } = t0;
+ let t1;
+ let t2;
+ if ($[0] !== a) {
+ t2 = { a };
+ $[0] = a;
+ $[1] = t2;
+ } else {
+ t2 = $[1];
+ }
+ t1 = t2;
+ const item1 = t1;
+ let t3;
+ let t4;
+ if ($[2] !== b) {
+ t4 = { b };
+ $[2] = b;
+ $[3] = t4;
+ } else {
+ t4 = $[3];
+ }
+ t3 = t4;
+ const item2 = t3;
+ useHook(item1, item2);
+ let t5;
+ if ($[4] !== a) {
+ t5 = [a];
+ $[4] = a;
+ $[5] = t5;
+ } else {
+ t5 = $[5];
+ }
+ let t6;
+ if ($[6] !== t5 || $[7] !== item1) {
+ t6 = ;
+ $[6] = t5;
+ $[7] = item1;
+ $[8] = t6;
+ } else {
+ t6 = $[8];
+ }
+ let t7;
+ if ($[9] !== b) {
+ t7 = [b];
+ $[9] = b;
+ $[10] = t7;
+ } else {
+ t7 = $[10];
+ }
+ let t8;
+ if ($[11] !== t7 || $[12] !== item2) {
+ t8 = ;
+ $[11] = t7;
+ $[12] = item2;
+ $[13] = t8;
+ } else {
+ t8 = $[13];
+ }
+ let t9;
+ if ($[14] !== t6 || $[15] !== t8) {
+ t9 = (
+ <>
+ {t6}
+ {t8}
+ >
+ );
+ $[14] = t6;
+ $[15] = t8;
+ $[16] = t9;
+ } else {
+ t9 = $[16];
+ }
+ return t9;
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: Component,
+ params: [{ a: 0, b: 0 }],
+ sequentialRenders: [
+ { a: 0, b: 0 },
+ { a: 1, b: 0 },
+ { a: 1, b: 1 },
+ { a: 1, b: 2 },
+ { a: 2, b: 2 },
+ { a: 3, b: 2 },
+ { a: 0, b: 0 },
+ ],
+};
+
+```
+
+### Eval output
+(kind: ok) [[ (exception in render) ReferenceError: ValidateMemoization is not defined ]]
+[[ (exception in render) ReferenceError: ValidateMemoization is not defined ]]
+[[ (exception in render) ReferenceError: ValidateMemoization is not defined ]]
+[[ (exception in render) ReferenceError: ValidateMemoization is not defined ]]
+[[ (exception in render) ReferenceError: ValidateMemoization is not defined ]]
+[[ (exception in render) ReferenceError: ValidateMemoization is not defined ]]
+[[ (exception in render) ReferenceError: ValidateMemoization is not defined ]]
+logs: [{ a: 0 },{ b: 0 },{ a: 0 },{ b: 0 },{ a: 1 },{ b: 1 },{ a: 1 },{ b: 2 },{ a: 2 },{ b: 2 },{ a: 3 },{ b: 2 },{ a: 0 },{ b: 0 }]
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-import-object-typed-module-as-hook-name.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-import-object-typed-module-as-hook-name.js
new file mode 100644
index 0000000000000..fb2eb66dee8b2
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-import-object-typed-module-as-hook-name.js
@@ -0,0 +1,29 @@
+import {useMemo} from 'react';
+import useHook from 'shared-runtime';
+
+export function Component({a, b}) {
+ const item1 = useMemo(() => ({a}), [a]);
+ const item2 = useMemo(() => ({b}), [b]);
+ useHook(item1, item2);
+
+ return (
+ <>
+
+
+ >
+ );
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: Component,
+ params: [{a: 0, b: 0}],
+ sequentialRenders: [
+ {a: 0, b: 0},
+ {a: 1, b: 0},
+ {a: 1, b: 1},
+ {a: 1, b: 2},
+ {a: 2, b: 2},
+ {a: 3, b: 2},
+ {a: 0, b: 0},
+ ],
+};
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-unknown-import-from-typed-module.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-unknown-import-from-typed-module.expect.md
new file mode 100644
index 0000000000000..3cf8a9f6e3e63
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-unknown-import-from-typed-module.expect.md
@@ -0,0 +1,144 @@
+
+## Input
+
+```javascript
+import {useMemo} from 'react';
+import {useHook, ValidateMemoization} from 'shared-runtime';
+
+export function Component({a, b}) {
+ const item1 = useMemo(() => ({a}), [a]);
+ const item2 = useMemo(() => ({b}), [b]);
+ useHook(item1, item2);
+
+ return (
+ <>
+
+
+ >
+ );
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: Component,
+ params: [{a: 0, b: 0}],
+ sequentialRenders: [
+ {a: 0, b: 0},
+ {a: 1, b: 0},
+ {a: 1, b: 1},
+ {a: 1, b: 2},
+ {a: 2, b: 2},
+ {a: 3, b: 2},
+ {a: 0, b: 0},
+ ],
+};
+
+```
+
+## Code
+
+```javascript
+import { c as _c } from "react/compiler-runtime";
+import { useMemo } from "react";
+import { useHook, ValidateMemoization } from "shared-runtime";
+
+export function Component(t0) {
+ const $ = _c(17);
+ const { a, b } = t0;
+ let t1;
+ let t2;
+ if ($[0] !== a) {
+ t2 = { a };
+ $[0] = a;
+ $[1] = t2;
+ } else {
+ t2 = $[1];
+ }
+ t1 = t2;
+ const item1 = t1;
+ let t3;
+ let t4;
+ if ($[2] !== b) {
+ t4 = { b };
+ $[2] = b;
+ $[3] = t4;
+ } else {
+ t4 = $[3];
+ }
+ t3 = t4;
+ const item2 = t3;
+ useHook(item1, item2);
+ let t5;
+ if ($[4] !== a) {
+ t5 = [a];
+ $[4] = a;
+ $[5] = t5;
+ } else {
+ t5 = $[5];
+ }
+ let t6;
+ if ($[6] !== t5 || $[7] !== item1) {
+ t6 = ;
+ $[6] = t5;
+ $[7] = item1;
+ $[8] = t6;
+ } else {
+ t6 = $[8];
+ }
+ let t7;
+ if ($[9] !== b) {
+ t7 = [b];
+ $[9] = b;
+ $[10] = t7;
+ } else {
+ t7 = $[10];
+ }
+ let t8;
+ if ($[11] !== t7 || $[12] !== item2) {
+ t8 = ;
+ $[11] = t7;
+ $[12] = item2;
+ $[13] = t8;
+ } else {
+ t8 = $[13];
+ }
+ let t9;
+ if ($[14] !== t6 || $[15] !== t8) {
+ t9 = (
+ <>
+ {t6}
+ {t8}
+ >
+ );
+ $[14] = t6;
+ $[15] = t8;
+ $[16] = t9;
+ } else {
+ t9 = $[16];
+ }
+ return t9;
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: Component,
+ params: [{ a: 0, b: 0 }],
+ sequentialRenders: [
+ { a: 0, b: 0 },
+ { a: 1, b: 0 },
+ { a: 1, b: 1 },
+ { a: 1, b: 2 },
+ { a: 2, b: 2 },
+ { a: 3, b: 2 },
+ { a: 0, b: 0 },
+ ],
+};
+
+```
+
+### Eval output
+(kind: ok) {"inputs":[0],"output":{"a":0}}
{"inputs":[0],"output":{"b":0}}
+{"inputs":[1],"output":{"a":1}}
{"inputs":[0],"output":{"b":0}}
+{"inputs":[1],"output":{"a":1}}
{"inputs":[1],"output":{"b":1}}
+{"inputs":[1],"output":{"a":1}}
{"inputs":[2],"output":{"b":2}}
+{"inputs":[2],"output":{"a":2}}
{"inputs":[2],"output":{"b":2}}
+{"inputs":[3],"output":{"a":3}}
{"inputs":[2],"output":{"b":2}}
+{"inputs":[0],"output":{"a":0}}
{"inputs":[0],"output":{"b":0}}
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-unknown-import-from-typed-module.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-unknown-import-from-typed-module.js
new file mode 100644
index 0000000000000..96902ffacfad5
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-unknown-import-from-typed-module.js
@@ -0,0 +1,29 @@
+import {useMemo} from 'react';
+import {useHook, ValidateMemoization} from 'shared-runtime';
+
+export function Component({a, b}) {
+ const item1 = useMemo(() => ({a}), [a]);
+ const item2 = useMemo(() => ({b}), [b]);
+ useHook(item1, item2);
+
+ return (
+ <>
+
+
+ >
+ );
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: Component,
+ params: [{a: 0, b: 0}],
+ sequentialRenders: [
+ {a: 0, b: 0},
+ {a: 1, b: 0},
+ {a: 1, b: 1},
+ {a: 1, b: 2},
+ {a: 2, b: 2},
+ {a: 3, b: 2},
+ {a: 0, b: 0},
+ ],
+};
diff --git a/compiler/packages/snap/src/sprout/shared-runtime-type-provider.ts b/compiler/packages/snap/src/sprout/shared-runtime-type-provider.ts
index fb0877d11474f..9a07902dbc2f5 100644
--- a/compiler/packages/snap/src/sprout/shared-runtime-type-provider.ts
+++ b/compiler/packages/snap/src/sprout/shared-runtime-type-provider.ts
@@ -48,6 +48,14 @@ export function makeSharedRuntimeTypeProvider({
returnType: {kind: 'type', name: 'Primitive'},
returnValueKind: ValueKindEnum.Primitive,
},
+ useArrayConcatNotTypedAsHook: {
+ kind: 'function',
+ calleeEffect: EffectEnum.Read,
+ positionalParams: [],
+ restParam: EffectEnum.Capture,
+ returnType: {kind: 'type', name: 'Array'},
+ returnValueKind: ValueKindEnum.Primitive,
+ },
useFreeze: {
kind: 'hook',
returnType: {kind: 'type', name: 'Any'},
diff --git a/compiler/packages/snap/src/sprout/shared-runtime.ts b/compiler/packages/snap/src/sprout/shared-runtime.ts
index bb1c65a6574ac..5b64e5ab46fb9 100644
--- a/compiler/packages/snap/src/sprout/shared-runtime.ts
+++ b/compiler/packages/snap/src/sprout/shared-runtime.ts
@@ -6,7 +6,7 @@
*/
import {IntlVariations, IntlViewerContext, init} from 'fbt';
-import React, {FunctionComponent} from 'react';
+import React, {FunctionComponent, useMemo} from 'react';
/**
* This file is meant for use by `runner-evaluator` and fixture tests.
@@ -355,4 +355,14 @@ export function typedArrayPush(array: Array, item: T): void {
export function typedLog(...values: Array): void {
console.log(...values);
}
+
+export function useArrayConcatNotTypedAsHook(
+ array1: Array,
+ array2: Array,
+): Array {
+ return useMemo(() => {
+ return [...array1, ...array2];
+ }, [array1, array2]);
+}
+
export default typedLog;